From 1f271af15da6238ff2be7e40800862134370e457 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Thu, 12 Dec 2024 21:16:07 +0800 Subject: [PATCH 01/10] feat: support cjs and esm both by tshy BREAKING CHANGE: drop Node.js < 18.19.0 support part of https://github.com/eggjs/egg/issues/3644 --- .eslintrc | 5 +- .github/PULL_REQUEST_TEMPLATE.md | 24 -- .github/workflows/codeql-analysis.yml | 72 ------ .github/workflows/nodejs.yml | 7 +- .github/workflows/release.yml | 6 +- .gitignore | 3 + CHANGELOG.md | 215 ++++++++++++++++++ History.md | 212 ----------------- README.md | 150 +++++++----- agent.js | 25 -- app.js | 115 ---------- lib/load_schedule.js | 60 ----- lib/schedule.js | 86 ------- lib/schedule_worker.js | 23 -- lib/strategy/all.js | 9 - lib/strategy/timer.js | 98 -------- lib/strategy/worker.js | 9 - package.json | 81 ++++--- src/agent.ts | 31 +++ src/app.ts | 131 +++++++++++ .../agent.js => src/app/extend/agent.ts | 14 +- .../app/extend/application.ts | 11 +- .../config/config.default.ts | 6 +- src/lib/load_schedule.ts | 69 ++++++ src/lib/schedule.ts | 90 ++++++++ src/lib/schedule_worker.ts | 24 ++ src/lib/strategy/all.ts | 7 + .../base.js => src/lib/strategy/base.ts | 42 ++-- src/lib/strategy/timer.ts | 106 +++++++++ src/lib/strategy/worker.ts | 7 + src/lib/types.ts | 50 ++++ test/{schedule.test.js => schedule.test.ts} | 23 +- tsconfig.json | 10 + 33 files changed, 943 insertions(+), 878 deletions(-) delete mode 100644 .github/PULL_REQUEST_TEMPLATE.md delete mode 100644 .github/workflows/codeql-analysis.yml delete mode 100644 History.md delete mode 100644 agent.js delete mode 100644 app.js delete mode 100644 lib/load_schedule.js delete mode 100644 lib/schedule.js delete mode 100644 lib/schedule_worker.js delete mode 100644 lib/strategy/all.js delete mode 100644 lib/strategy/timer.js delete mode 100644 lib/strategy/worker.js create mode 100644 src/agent.ts create mode 100644 src/app.ts rename app/extend/agent.js => src/app/extend/agent.ts (65%) rename app/extend/application.js => src/app/extend/application.ts (65%) rename config/config.default.js => src/config/config.default.ts (77%) create mode 100644 src/lib/load_schedule.ts create mode 100644 src/lib/schedule.ts create mode 100644 src/lib/schedule_worker.ts create mode 100644 src/lib/strategy/all.ts rename lib/strategy/base.js => src/lib/strategy/base.ts (56%) create mode 100644 src/lib/strategy/timer.ts create mode 100644 src/lib/strategy/worker.ts create mode 100644 src/lib/types.ts rename test/{schedule.test.js => schedule.test.ts} (98%) create mode 100644 tsconfig.json diff --git a/.eslintrc b/.eslintrc index c799fe5..9bcdb46 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,3 +1,6 @@ { - "extends": "eslint-config-egg" + "extends": [ + "eslint-config-egg/typescript", + "eslint-config-egg/lib/rules/enforce-node-prefix" + ] } diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 48f9944..0000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,24 +0,0 @@ - - -##### Checklist - - -- [ ] `npm test` passes -- [ ] tests and/or benchmarks are included -- [ ] documentation is changed or added -- [ ] commit message follows commit guidelines - -##### Affected core subsystem(s) - - - -##### Description of change - diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 82a4735..0000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,72 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - push: - branches: [ "master" ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ "master" ] - schedule: - - cron: '40 5 * * 2' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'javascript' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] - # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality - - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - - # - run: | - # echo "Run, Build Application using script" - # ./location_of_script_within_repo/buildscript.sh - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index f38bb73..dc5563d 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -3,16 +3,13 @@ name: CI on: push: branches: [ master ] - pull_request: branches: [ master ] - workflow_dispatch: {} - jobs: Job: name: Node.js - uses: artusjs/github-actions/.github/workflows/node-test.yml@v1 + uses: node-modules/github-actions/.github/workflows/node-test.yml@master with: os: 'ubuntu-latest' - version: '14, 16, 18, 20' + version: '18, 20, 22' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1612587..a2bf04a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,14 +4,10 @@ on: push: branches: [ master ] - workflow_dispatch: {} - jobs: release: name: Node.js - uses: artusjs/github-actions/.github/workflows/node-release.yml@v1 + uses: eggjs/github-actions/.github/workflows/node-release.yml@master secrets: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} GIT_TOKEN: ${{ secrets.GIT_TOKEN }} - with: - checkTest: false diff --git a/.gitignore b/.gitignore index 8448054..00eab48 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,6 @@ run/ test/fixtures/symlink/runDir/app/schedule/realFile.js test/fixtures/symlink/runDir/app/schedule/tsRealFile.ts package-lock.json +.tshy* +dist +.eslintcache diff --git a/CHANGELOG.md b/CHANGELOG.md index ce425b6..7e5b9ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,3 +6,218 @@ ### Bug Fixes * schedule should execute after app ready ([#60](https://github.com/eggjs/egg-schedule/issues/60)) ([bf01a49](https://github.com/eggjs/egg-schedule/commit/bf01a49b093b4a32ee546b64be3059f0dbe65572)) + +--- + + +4.0.0 / 2022-12-11 +================== + +**features** + * [[`66e7aeb`](http://github.com/eggjs/egg-schedule/commit/66e7aeb87dfc8994529ec9d8407a219461345d27)] - 📦 NEW: [BREAKING] Support localStorage to run task (#58) (fengmk2 <>) + +**others** + * [[`7672269`](http://github.com/eggjs/egg-schedule/commit/7672269168f2a19a5e239fc58e625b32f89693cc)] - Create codeql-analysis.yml (fengmk2 <>) + * [[`bab8585`](http://github.com/eggjs/egg-schedule/commit/bab8585a64dbc7d7c0ff1c1b88b5dc544b9e8aac)] - ci: del appveyor (#57) (killa <>) + +3.7.0 / 2022-09-03 +================== + +**features** + * [[`ca8b92b`](http://github.com/eggjs/egg-schedule/commit/ca8b92be7e72f3b35e80d8812d378f822f7a02b2)] - feat: add api for register/unregister schedule (#56) (killa <>) + +3.6.6 / 2020-10-23 +================== + +**fixes** + * [[`3a8ef55`](http://github.com/eggjs/egg-schedule/commit/3a8ef55ff3da580f277755fdb0cca15a12fc2256)] - fix: runSchedule get filePath keep same logic with loader (#55) (mansonchor.github.com <>) + +**others** + * [[`39f4aad`](http://github.com/eggjs/egg-schedule/commit/39f4aadf1f10736a4fa3cedd779a48cb329d5320)] - docs: updated README with grammatical and spelling fixes (#54) (Hridayesh Sharma <>) + +3.6.5 / 2020-09-01 +================== + +**fixes** + * [[`7ee8cb3`](http://github.com/eggjs/egg-schedule/commit/7ee8cb3696751ca30bbc3abfb3cbc06a676d2fe7)] - fix: only reject/error detect as fail (#53) (TZ | 天猪 <>) + +**others** + * [[`0425031`](http://github.com/eggjs/egg-schedule/commit/0425031279bc98a716671111e3f36bdd0a353017)] - docs: add module exports for custom schedule in document (#52) (Cheng Ju Wu <>) + * [[`7da04fe`](http://github.com/eggjs/egg-schedule/commit/7da04fe5b3adfd572375b02f1063b8d90684743e)] - chore: Update README.md (#51) (Cheng Ju Wu <>) + * [[`13f03b8`](http://github.com/eggjs/egg-schedule/commit/13f03b8681d60dfa8c9d33574cef392dc55c4b27)] - chore: Update .travis.yml (#50) (TZ | 天猪 <>) + +3.6.4 / 2019-06-12 +================== + +**fixes** + * [[`b6b17b0`](http://github.com/eggjs/egg-schedule/commit/b6b17b032582dbde6f4f9faecef3dd662726b3e2)] - fix: should use template literal (#49) (Jedmeng <>) + +3.6.3 / 2019-06-03 +================== + +**fixes** + * [[`bce393f`](http://github.com/eggjs/egg-schedule/commit/bce393f66f70add9151155d16b90942bab75e89b)] - fix: wrap task should always return promise (#48) (TZ | 天猪 <>) + +3.6.2 / 2019-04-29 +================== + +**fixes** + * [[`98a0cf7`](http://github.com/eggjs/egg-schedule/commit/98a0cf78012bd138cd8b0893c1acb6527cfecf0e)] - fix: runSchedule should pass args (#47) (TZ | 天猪 <>) + +**others** + * [[`77fc7d3`](http://github.com/eggjs/egg-schedule/commit/77fc7d3b46d1c57acc69cb3931241f9cb8165a38)] - docs: fix ctx ref (#46) (祝传鹏 <>) + +3.6.1 / 2019-03-20 +================== + +**others** + * [[`0960ff8`](http://github.com/eggjs/egg-schedule/commit/0960ff8f058c2bbb8ad2afb333bc557d719ec99b)] - chore: use relative log path (#45) (TZ | 天猪 <>) + +3.6.0 / 2018-12-18 +================== + +**features** + * [[`2c64d3c`](https://github.com/eggjs/egg-schedule/commit/2c64d3c6dd386dedaa784180ebb6c61b89fd1d53)] - feat: support custom directory (#43) (TZ <>) + +3.5.0 / 2018-12-05 +================== + +**features** + * [[`1aaf2d5`](http://github.com/eggjs/egg-schedule/commit/1aaf2d5675253d125eacca8bfd77813ecc151d2a)] - feat: support custom directory (#43) (Haoliang Gao <>) + +**fixes** + * [[`4dbf9d9`](http://github.com/eggjs/egg-schedule/commit/4dbf9d9d3785b19eb772704c724c421e1017922a)] - fix: unit-test in 'schedule.test.js' (#41) (Maledong <>) + +**others** + * [[`571bf9f`](http://github.com/eggjs/egg-schedule/commit/571bf9f28ed229f957fa70067786061a89dc1049)] - doc: Add notice for the evil 'setInterval' (#42) (Maledong <>) + * [[`07e4e23`](http://github.com/eggjs/egg-schedule/commit/07e4e238f198fbf935ac5e7fff279f349e11a6b5)] - docs: fix example in readme (cwtuan <>) + +3.4.0 / 2018-06-30 +================== + +**features** + * [[`417a764`](http://github.com/eggjs/egg-schedule/commit/417a7643807e56a432703e64f76923b60e1053ba)] - feat: support `schedule.env` (#39) (TZ | 天猪 <>) + +3.3.0 / 2018-02-24 +================== + + * feat: optimize logger msg (#38) + +3.2.1 / 2018-02-07 +================== + + * chore: fix doctools (#37) + +3.2.0 / 2018-02-06 +================== + +**features** + * [[`2003369`](http://github.com/eggjs/egg-schedule/commit/200336963cdf2404b926fa1c36223c41229cf32d)] - feat: egg-schedule.log && support send with args (#35) (TZ | 天猪 <>) + +3.1.1 / 2017-11-20 +================== + +**fixes** + * [[`9ff3974`](http://github.com/eggjs/egg-schedule/commit/9ff3974683e1f4ade72ccbe2448a3c68d7826530)] - fix: use ctx.coreLogger to record schedule log (#34) (Yiyu He <>) + +3.1.0 / 2017-11-16 +================== + +**features** + * [[`69a588e`](https://github.com/eggjs/egg-schedule/commit/69a588e5ffbb5a01ed3084bfb9f6c2a792963db4)] - feat: run a scheduler only once at startup (#33) (zhennann <>) + +3.0.0 / 2017-11-10 +================== + +**others** + * [[`925f1c3`](http://github.com/eggjs/egg-schedule/commit/925f1c38ffb5c8d73e91fe96e6e7fc30c3f43c5f)] - refactor: remove old stype strategy support [BREAKING CHANGE] (#29) (TZ | 天猪 <>) + * [[`4cdfa20`](http://github.com/eggjs/egg-schedule/commit/4cdfa204f1da36288328bf30acb0564da1e3d1b5)] - test: change to extend Subscription (#28) (TZ | 天猪 <>) + +2.6.0 / 2017-10-16 +================== + +**features** + * [[`f901df4`](http://github.com/eggjs/egg-schedule/commit/f901df4e895d440c9d3bc96e172d3cc87be95255)] - feat: Strategy interface change to start() (#26) (TZ | 天猪 <>) + * [[`c7816f2`](http://github.com/eggjs/egg-schedule/commit/c7816f2eb8ca668c92c1671b1d149c78dd73551e)] - feat: support class (#25) (Haoliang Gao <>) + +**others** + * [[`8797489`](http://github.com/eggjs/egg-schedule/commit/8797489f914a34bf56ecc68575b0b7e490628b5a)] - docs: use subscription (#27) (Haoliang Gao <>) + +2.5.1 / 2017-10-11 +================== + + * fix: publish files (#24) + +2.5.0 / 2017-10-11 +================== + + * refactor: classify (#23) + * test: sleep after runSchedule (#22) + +2.4.1 / 2017-06-06 +================== + + * fix: use safe-timers only large than interval && add tests (#21) + +2.4.0 / 2017-06-05 +================== + + * feat: use safe-timers to support large delay (#19) + +2.3.1 / 2017-06-04 +================== + + * docs: fix License url (#20) + * test: fix test on windows (#18) + * chore: upgrade all deps (#17) + +2.3.0 / 2017-02-08 +================== + + * feat: task support async function (#13) + * test: move app.close to afterEach (#12) + * chore: upgrade deps and fix test (#11) + +2.2.1 / 2016-10-25 +================== + + * fix: start schedule after egg-ready (#10) + +2.2.0 / 2016-09-29 +================== + + * feat: export app.schedules (#9) + * doc:fix plugin.js config demo (#8) + +2.1.0 / 2016-08-18 +================== + + * refactor: use FileLoader to load schedule files (#7) + +2.0.0 / 2016-08-16 +================== + + * Revert "Release 1.1.1" + * refactor: use loader.getLoadUnits from egg-core (#6) + +1.1.0 / 2016-08-15 +================== + + * docs: add readme (#5) + * feat: support immediate (#4) + +1.0.0 / 2016-08-10 +================== + + * fix: correct path in ctx (#3) + +0.1.0 / 2016-07-26 +================== + + * fix: use absolute path for store key (#2) + * test: add test cases (#1) + +0.0.1 / 2016-07-15 +================== + + * init diff --git a/History.md b/History.md deleted file mode 100644 index 5ec0039..0000000 --- a/History.md +++ /dev/null @@ -1,212 +0,0 @@ - -4.0.0 / 2022-12-11 -================== - -**features** - * [[`66e7aeb`](http://github.com/eggjs/egg-schedule/commit/66e7aeb87dfc8994529ec9d8407a219461345d27)] - 📦 NEW: [BREAKING] Support localStorage to run task (#58) (fengmk2 <>) - -**others** - * [[`7672269`](http://github.com/eggjs/egg-schedule/commit/7672269168f2a19a5e239fc58e625b32f89693cc)] - Create codeql-analysis.yml (fengmk2 <>) - * [[`bab8585`](http://github.com/eggjs/egg-schedule/commit/bab8585a64dbc7d7c0ff1c1b88b5dc544b9e8aac)] - ci: del appveyor (#57) (killa <>) - -3.7.0 / 2022-09-03 -================== - -**features** - * [[`ca8b92b`](http://github.com/eggjs/egg-schedule/commit/ca8b92be7e72f3b35e80d8812d378f822f7a02b2)] - feat: add api for register/unregister schedule (#56) (killa <>) - -3.6.6 / 2020-10-23 -================== - -**fixes** - * [[`3a8ef55`](http://github.com/eggjs/egg-schedule/commit/3a8ef55ff3da580f277755fdb0cca15a12fc2256)] - fix: runSchedule get filePath keep same logic with loader (#55) (mansonchor.github.com <>) - -**others** - * [[`39f4aad`](http://github.com/eggjs/egg-schedule/commit/39f4aadf1f10736a4fa3cedd779a48cb329d5320)] - docs: updated README with grammatical and spelling fixes (#54) (Hridayesh Sharma <>) - -3.6.5 / 2020-09-01 -================== - -**fixes** - * [[`7ee8cb3`](http://github.com/eggjs/egg-schedule/commit/7ee8cb3696751ca30bbc3abfb3cbc06a676d2fe7)] - fix: only reject/error detect as fail (#53) (TZ | 天猪 <>) - -**others** - * [[`0425031`](http://github.com/eggjs/egg-schedule/commit/0425031279bc98a716671111e3f36bdd0a353017)] - docs: add module exports for custom schedule in document (#52) (Cheng Ju Wu <>) - * [[`7da04fe`](http://github.com/eggjs/egg-schedule/commit/7da04fe5b3adfd572375b02f1063b8d90684743e)] - chore: Update README.md (#51) (Cheng Ju Wu <>) - * [[`13f03b8`](http://github.com/eggjs/egg-schedule/commit/13f03b8681d60dfa8c9d33574cef392dc55c4b27)] - chore: Update .travis.yml (#50) (TZ | 天猪 <>) - -3.6.4 / 2019-06-12 -================== - -**fixes** - * [[`b6b17b0`](http://github.com/eggjs/egg-schedule/commit/b6b17b032582dbde6f4f9faecef3dd662726b3e2)] - fix: should use template literal (#49) (Jedmeng <>) - -3.6.3 / 2019-06-03 -================== - -**fixes** - * [[`bce393f`](http://github.com/eggjs/egg-schedule/commit/bce393f66f70add9151155d16b90942bab75e89b)] - fix: wrap task should always return promise (#48) (TZ | 天猪 <>) - -3.6.2 / 2019-04-29 -================== - -**fixes** - * [[`98a0cf7`](http://github.com/eggjs/egg-schedule/commit/98a0cf78012bd138cd8b0893c1acb6527cfecf0e)] - fix: runSchedule should pass args (#47) (TZ | 天猪 <>) - -**others** - * [[`77fc7d3`](http://github.com/eggjs/egg-schedule/commit/77fc7d3b46d1c57acc69cb3931241f9cb8165a38)] - docs: fix ctx ref (#46) (祝传鹏 <>) - -3.6.1 / 2019-03-20 -================== - -**others** - * [[`0960ff8`](http://github.com/eggjs/egg-schedule/commit/0960ff8f058c2bbb8ad2afb333bc557d719ec99b)] - chore: use relative log path (#45) (TZ | 天猪 <>) - -3.6.0 / 2018-12-18 -================== - -**features** - * [[`2c64d3c`](https://github.com/eggjs/egg-schedule/commit/2c64d3c6dd386dedaa784180ebb6c61b89fd1d53)] - feat: support custom directory (#43) (TZ <>) - -3.5.0 / 2018-12-05 -================== - -**features** - * [[`1aaf2d5`](http://github.com/eggjs/egg-schedule/commit/1aaf2d5675253d125eacca8bfd77813ecc151d2a)] - feat: support custom directory (#43) (Haoliang Gao <>) - -**fixes** - * [[`4dbf9d9`](http://github.com/eggjs/egg-schedule/commit/4dbf9d9d3785b19eb772704c724c421e1017922a)] - fix: unit-test in 'schedule.test.js' (#41) (Maledong <>) - -**others** - * [[`571bf9f`](http://github.com/eggjs/egg-schedule/commit/571bf9f28ed229f957fa70067786061a89dc1049)] - doc: Add notice for the evil 'setInterval' (#42) (Maledong <>) - * [[`07e4e23`](http://github.com/eggjs/egg-schedule/commit/07e4e238f198fbf935ac5e7fff279f349e11a6b5)] - docs: fix example in readme (cwtuan <>) - -3.4.0 / 2018-06-30 -================== - -**features** - * [[`417a764`](http://github.com/eggjs/egg-schedule/commit/417a7643807e56a432703e64f76923b60e1053ba)] - feat: support `schedule.env` (#39) (TZ | 天猪 <>) - -3.3.0 / 2018-02-24 -================== - - * feat: optimize logger msg (#38) - -3.2.1 / 2018-02-07 -================== - - * chore: fix doctools (#37) - -3.2.0 / 2018-02-06 -================== - -**features** - * [[`2003369`](http://github.com/eggjs/egg-schedule/commit/200336963cdf2404b926fa1c36223c41229cf32d)] - feat: egg-schedule.log && support send with args (#35) (TZ | 天猪 <>) - -3.1.1 / 2017-11-20 -================== - -**fixes** - * [[`9ff3974`](http://github.com/eggjs/egg-schedule/commit/9ff3974683e1f4ade72ccbe2448a3c68d7826530)] - fix: use ctx.coreLogger to record schedule log (#34) (Yiyu He <>) - -3.1.0 / 2017-11-16 -================== - -**features** - * [[`69a588e`](https://github.com/eggjs/egg-schedule/commit/69a588e5ffbb5a01ed3084bfb9f6c2a792963db4)] - feat: run a scheduler only once at startup (#33) (zhennann <>) - -3.0.0 / 2017-11-10 -================== - -**others** - * [[`925f1c3`](http://github.com/eggjs/egg-schedule/commit/925f1c38ffb5c8d73e91fe96e6e7fc30c3f43c5f)] - refactor: remove old stype strategy support [BREAKING CHANGE] (#29) (TZ | 天猪 <>) - * [[`4cdfa20`](http://github.com/eggjs/egg-schedule/commit/4cdfa204f1da36288328bf30acb0564da1e3d1b5)] - test: change to extend Subscription (#28) (TZ | 天猪 <>) - -2.6.0 / 2017-10-16 -================== - -**features** - * [[`f901df4`](http://github.com/eggjs/egg-schedule/commit/f901df4e895d440c9d3bc96e172d3cc87be95255)] - feat: Strategy interface change to start() (#26) (TZ | 天猪 <>) - * [[`c7816f2`](http://github.com/eggjs/egg-schedule/commit/c7816f2eb8ca668c92c1671b1d149c78dd73551e)] - feat: support class (#25) (Haoliang Gao <>) - -**others** - * [[`8797489`](http://github.com/eggjs/egg-schedule/commit/8797489f914a34bf56ecc68575b0b7e490628b5a)] - docs: use subscription (#27) (Haoliang Gao <>) - -2.5.1 / 2017-10-11 -================== - - * fix: publish files (#24) - -2.5.0 / 2017-10-11 -================== - - * refactor: classify (#23) - * test: sleep after runSchedule (#22) - -2.4.1 / 2017-06-06 -================== - - * fix: use safe-timers only large than interval && add tests (#21) - -2.4.0 / 2017-06-05 -================== - - * feat: use safe-timers to support large delay (#19) - -2.3.1 / 2017-06-04 -================== - - * docs: fix License url (#20) - * test: fix test on windows (#18) - * chore: upgrade all deps (#17) - -2.3.0 / 2017-02-08 -================== - - * feat: task support async function (#13) - * test: move app.close to afterEach (#12) - * chore: upgrade deps and fix test (#11) - -2.2.1 / 2016-10-25 -================== - - * fix: start schedule after egg-ready (#10) - -2.2.0 / 2016-09-29 -================== - - * feat: export app.schedules (#9) - * doc:fix plugin.js config demo (#8) - -2.1.0 / 2016-08-18 -================== - - * refactor: use FileLoader to load schedule files (#7) - -2.0.0 / 2016-08-16 -================== - - * Revert "Release 1.1.1" - * refactor: use loader.getLoadUnits from egg-core (#6) - -1.1.0 / 2016-08-15 -================== - - * docs: add readme (#5) - * feat: support immediate (#4) - -1.0.0 / 2016-08-10 -================== - - * fix: correct path in ctx (#3) - -0.1.0 / 2016-07-26 -================== - - * fix: use absolute path for store key (#2) - * test: add test cases (#1) - -0.0.1 / 2016-07-15 -================== - - * init diff --git a/README.md b/README.md index 76069bf..b680ed9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# egg-schedule +# @eggjs/schedule [![NPM version][npm-image]][npm-url] [![Node.js CI](https://github.com/eggjs/egg-schedule/actions/workflows/nodejs.yml/badge.svg)](https://github.com/eggjs/egg-schedule/actions/workflows/nodejs.yml) @@ -6,14 +6,14 @@ [![Known Vulnerabilities][snyk-image]][snyk-url] [![npm download][download-image]][download-url] -[npm-image]: https://img.shields.io/npm/v/egg-schedule.svg?style=flat-square -[npm-url]: https://npmjs.org/package/egg-schedule +[npm-image]: https://img.shields.io/npm/v/@eggjs/schedule.svg?style=flat-square +[npm-url]: https://npmjs.org/package/@eggjs/schedule [codecov-image]: https://codecov.io/github/eggjs/egg-schedule/coverage.svg?branch=master [codecov-url]: https://codecov.io/github/eggjs/egg-schedule?branch=master [snyk-image]: https://snyk.io/test/npm/egg-schedule/badge.svg?style=flat-square -[snyk-url]: https://snyk.io/test/npm/egg-schedule -[download-image]: https://img.shields.io/npm/dm/egg-schedule.svg?style=flat-square -[download-url]: https://npmjs.org/package/egg-schedule +[snyk-url]: https://snyk.io/test/npm/@eggjs/schedule +[download-image]: https://img.shields.io/npm/dm/@eggjs/schedule.svg?style=flat-square +[download-url]: https://npmjs.org/package/@eggjs/schedule A schedule plugin for egg, has been built-in plugin for egg enabled by default. @@ -21,13 +21,13 @@ It's fully extendable for a developer and provides a simple built-in TimerStrate ## Usage -Just add your job file to `{app_root}/app/schedule`. +Just add your job file to `{baseDir}/app/schedule`. -```js -// {app_root}/app/schedule/cleandb.js -const Subscription = require('egg').Subscription; +```ts +// {baseDir}/app/schedule/cleandb.ts +import { Subscription } from 'egg'; -class CleanDB extends Subscription { +export default class CleanDB extends Subscription { /** * @property {Object} schedule * - {String} type - schedule type, `worker` or `all` or your custom types. @@ -51,28 +51,28 @@ class CleanDB extends Subscription { await this.ctx.service.db.cleandb(); } } - -module.exports = CleanDB; ``` -You can also use function simply like: +You can also use function simply like: + +```ts +import { EggContext } from 'egg'; -```js -exports.schedule = { +export const schedule = { type: 'worker', cron: '0 0 3 * * *', // interval: '1h', // immediate: true, -}; +} -exports.task = async function (ctx) { +export async function task(ctx: EggContext) { await ctx.service.db.cleandb(); -}; +} ``` ## Overview -`egg-schedule` supports both cron-based scheduling and interval-based scheduling. +`@eggjs/schedule` supports both cron-based scheduling and interval-based scheduling. Schedule decision is being made by `agent` process. `agent` triggers a task and sends a message to `worker` process. Then, one or all `worker` process(es) execute the task based on schedule type. @@ -91,21 +91,23 @@ You can get anonymous context with `this.ctx`. To create a task, `subscribe` can be a generator function or async function. For example: -```js +```ts // A simple logger example -const Subscription = require('egg').Subscription; -class LoggerExample extends Subscription { +import { Subscription } from 'egg'; + +export default class LoggerExample extends Subscription { async subscribe() { this.ctx.logger.info('Info about your task'); } } ``` -```js +```ts // A real world example: wipe out your database. // Use it with caution. :) -const Subscription = require('egg').Subscription; -class CleanDB extends Subscription { +import { Subscription } from 'egg'; + +export default class CleanDB extends Subscription { async subscribe() { await this.ctx.service.db.cleandb(); } @@ -138,14 +140,14 @@ Use [cron-parser](https://github.com/harrisiirak/cron-parser). Example: -```js +```ts // To execute task every 3 hours -exports.schedule = { +export const schedule = { type: 'worker', cron: '0 0 */3 * * *', cronOptions: { // tz: 'Europe/Athens', - } + }, }; ``` @@ -155,9 +157,9 @@ To use `setInterval`, and support [ms](https://www.npmjs.com/package/ms) convers Example: -```js +```ts // To execute task every 3 hours -exports.schedule = { +export const schedule = { type: 'worker', interval: '3h', }; @@ -175,11 +177,15 @@ exports.schedule = { **Custom schedule:** To create a custom schedule, simply extend `agent.ScheduleStrategy` and register it by `agent.schedule.use(type, clz)`. -You can schedule the task to be executed by one random worker or all workers with the built-in method `this.sendOne(...args)` or `this.sendAll(...args)` which support params, it will pass to `subscribe(...args)` or `task(ctx, ...args)`. +You can schedule the task to be executed by one random worker or all workers with +the built-in method `this.sendOne(...args)` or `this.sendAll(...args)` which support params, +it will pass to `subscribe(...args)` or `task(ctx, ...args)`. + +```ts +// {baseDir}/agent.ts +import { Agent } from 'egg'; -```js -// {app_root}/agent.js -module.exports = function(agent) { +export default (agent: Agent) => { class CustomStrategy extends agent.ScheduleStrategy { start() { // such as mq / redis subscribe @@ -188,34 +194,38 @@ module.exports = function(agent) { }); } } + agent.schedule.use('custom', CustomStrategy); -}; +} ``` Then you could use it to defined your job: -```js -// {app_root}/app/schedule/other.js -const Subscription = require('egg').Subscription; -class ClusterTask extends Subscription { +```ts +// {baseDir}/app/schedule/other.ts +import { Subscription } from 'egg'; + +export default class ClusterTask extends Subscription { static get schedule() { return { type: 'custom', }; } + async subscribe(data) { console.log('got custom data:', data); await this.ctx.service.someTask.run(); } } -module.exports = ClusterTask; ``` ## Dynamic schedule -```js -// {app_root}/app/schedule/sync.js -module.exports = app => { +```ts +// {baseDir}/app/schedule/sync.ts +import { Application } from 'egg'; + +export default (app: Application) => { class SyncTask extends app.Subscription { static get schedule() { return { @@ -227,10 +237,12 @@ module.exports = app => { env: [ 'prod' ], }; } + async subscribe() { await this.ctx.sync(); } } + return SyncTask; } ``` @@ -239,39 +251,47 @@ module.exports = app => { ### Logging -See `${appInfo.root}/logs/{app_name}/egg-schedule.log` which provided by [config.customLogger.scheduleLogger](https://github.com/eggjs/egg-schedule/blob/master/config/config.default.js). +See `${appInfo.root}/logs/{app_name}/egg-schedule.log` which provided by [config.customLogger.scheduleLogger](https://github.com/eggjs/egg-schedule/blob/master/config/config.default.ts). + +```ts +// config/config.default.ts +import { EggAppConfig } from 'egg'; -```js -// config/config.default.js -config.customLogger = { - scheduleLogger: { - // consoleLevel: 'NONE', - // file: path.join(appInfo.root, 'logs', appInfo.name, 'egg-schedule.log'), +export default const config = { + customLogger: { + scheduleLogger: { + // consoleLevel: 'NONE', + // file: path.join(appInfo.root, 'logs', appInfo.name, 'egg-schedule.log'), + }, }, -}; +} as Partial; ``` ### Customize directory If you want to add additional schedule directories, you can use this config. -```js -// config/config.default.js -config.schedule = { - directory: [ - path.join(__dirname, '../app/otherSchedule'), - ], -}; +```ts +// config/config.default.ts +import { EggAppConfig } from 'egg'; + +export default const config = { + schedule: { + directory: [ + 'path/to/otherSchedule', + ], + }, +} as Partial; ``` ## Testing -`app.runSchedule(scheduleName)` is provided by `egg-schedule` plugin only for test purpose. +`app.runSchedule(scheduleName)` is provided by `@eggjs/schedule` plugin only for test purpose. Example: -```js -it('test a schedule task', async function () { +```ts +it('test a schedule task', async () => { // get app instance await app.runSchedule('clean_cache'); }); @@ -284,3 +304,9 @@ Please open an issue [here](https://github.com/eggjs/egg/issues). ## License [MIT](https://github.com/eggjs/egg-schedule/blob/master/LICENSE) + +## Contributors + +[![Contributors](https://contrib.rocks/image?repo=eggjs/egg-schedule)](https://github.com/eggjs/egg-schedule/graphs/contributors) + +Made with [contributors-img](https://contrib.rocks). diff --git a/agent.js b/agent.js deleted file mode 100644 index 730edfd..0000000 --- a/agent.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict'; - -const WorkerStrategy = require('./lib/strategy/worker'); -const AllStrategy = require('./lib/strategy/all'); - -module.exports = agent => { - // register built-in strategy - agent.schedule.use('worker', WorkerStrategy); - agent.schedule.use('all', AllStrategy); - - // wait for other plugin to register custom strategy - agent.beforeStart(() => { - agent.schedule.init(); - }); - - // dispatch job finish event to strategy - agent.messenger.on('egg-schedule', (...args) => { - agent.schedule.onJobFinish(...args); - }); - - agent.messenger.once('egg-ready', () => { - // start schedule after worker ready - agent.schedule.start(); - }); -}; diff --git a/app.js b/app.js deleted file mode 100644 index aadf4e3..0000000 --- a/app.js +++ /dev/null @@ -1,115 +0,0 @@ -'use strict'; - -const qs = require('querystring'); -const path = require('path'); -const is = require('is-type-of'); - -module.exports = app => { - const logger = app.getLogger('scheduleLogger'); - const scheduleWorker = app.scheduleWorker; - scheduleWorker.init(); - - // log schedule list - for (const s in scheduleWorker.scheduleItems) { - const schedule = scheduleWorker.scheduleItems[s]; - if (!schedule.schedule.disable) logger.info('[egg-schedule]: register schedule %s', schedule.key); - } - - // register schedule event - app.messenger.on('egg-schedule', async info => { - const { id, key } = info; - logger.debug(`[Job#${id}] ${key} await app ready`); - await app.ready(); - const schedule = scheduleWorker.scheduleItems[key]; - logger.debug(`[Job#${id}] ${key} task received by app`); - - if (!schedule) { - logger.warn(`[Job#${id}] ${key} unknown task`); - return; - } - - /* istanbul ignore next */ - if (schedule.schedule.disable) { - logger.warn(`[Job#${id}] ${key} disable`); - return; - } - - logger.info(`[Job#${id}] ${key} executing by app`); - - // run with anonymous context - const ctx = app.createAnonymousContext({ - method: 'SCHEDULE', - url: `/__schedule?path=${key}&${qs.stringify(schedule.schedule)}`, - }); - - const start = Date.now(); - - let success; - let e; - try { - // execute - await app.ctxStorage.run(ctx, async () => { - return await schedule.task(ctx, ...info.args); - }); - success = true; - } catch (err) { - success = false; - e = is.error(err) ? err : new Error(err); - } - - const rt = Date.now() - start; - - const msg = `[Job#${id}] ${key} execute ${success ? 'succeed' : 'failed'}, used ${rt}ms.`; - logger[success ? 'info' : 'error'](msg, success ? '' : e); - - Object.assign(info, { - success, - workerId: process.pid, - rt, - message: e && e.message, - }); - - // notify agent job finish - app.messenger.sendToAgent('egg-schedule', info); - }); - - // for test purpose - const directory = [].concat(path.join(app.config.baseDir, 'app/schedule'), app.config.schedule.directory || []); - app.runSchedule = (schedulePath, ...args) => { - // resolve real path - if (path.isAbsolute(schedulePath)) { - schedulePath = require.resolve(schedulePath); - } else { - for (const dir of directory) { - try { - schedulePath = require.resolve(path.join(dir, schedulePath)); - break; - } catch (_) { - /* istanbul ignore next */ - } - } - } - - let schedule; - - try { - schedule = scheduleWorker.scheduleItems[schedulePath]; - if (!schedule) { - throw new Error(`Cannot find schedule ${schedulePath}`); - } - } catch (err) { - err.message = `[egg-schedule] ${err.message}`; - return Promise.reject(err); - } - - // run with anonymous context - const ctx = app.createAnonymousContext({ - method: 'SCHEDULE', - url: `/__schedule?path=${schedulePath}&${qs.stringify(schedule.schedule)}`, - }); - - return app.ctxStorage.run(ctx, () => { - return schedule.task(ctx, ...args); - }); - }; -}; diff --git a/lib/load_schedule.js b/lib/load_schedule.js deleted file mode 100644 index 8b9fbda..0000000 --- a/lib/load_schedule.js +++ /dev/null @@ -1,60 +0,0 @@ -'use strict'; - -const path = require('path'); -const assert = require('assert'); -const is = require('is-type-of'); - -module.exports = app => { - const dirs = app.loader.getLoadUnits().map(unit => path.join(unit.path, 'app/schedule')); - dirs.push(...app.config.schedule.directory); - - const Loader = getScheduleLoader(app); - const schedules = app.schedules = {}; - new Loader({ - directory: dirs, - target: schedules, - inject: app, - }).load(); - return schedules; -}; - -function getScheduleLoader(app) { - return class ScheduleLoader extends app.loader.FileLoader { - load() { - const target = this.options.target; - const items = this.parse(); - for (const item of items) { - const schedule = item.exports; - const fullpath = item.fullpath; - assert(schedule.schedule, `schedule(${fullpath}): must have schedule and task properties`); - assert(is.class(schedule) || is.function(schedule.task), `schedule(${fullpath}: schedule.task should be function or schedule should be class`); - - let task; - if (is.class(schedule)) { - task = async (ctx, data) => { - const s = new schedule(ctx); - s.subscribe = app.toAsyncFunction(s.subscribe); - return s.subscribe(data); - }; - } else { - task = app.toAsyncFunction(schedule.task); - } - - const env = app.config.env; - const envList = schedule.schedule.env; - if (is.array(envList) && !envList.includes(env)) { - app.coreLogger.info(`[egg-schedule]: ignore schedule ${fullpath} due to \`schedule.env\` not match`); - continue; - } - - // handle symlink case - const realFullpath = require.resolve(fullpath); - target[realFullpath] = { - schedule: schedule.schedule, - task, - key: realFullpath, - }; - } - } - }; -} diff --git a/lib/schedule.js b/lib/schedule.js deleted file mode 100644 index 8d1fdd3..0000000 --- a/lib/schedule.js +++ /dev/null @@ -1,86 +0,0 @@ -'use strict'; - -const STRATEGY = Symbol('strategy'); -const STRATEGY_INSTANCE = Symbol('strategy_instance'); -const loadSchedule = require('./load_schedule'); - -module.exports = class Schedule { - constructor(agent) { - this.agent = agent; - this.logger = agent.getLogger('scheduleLogger'); - this[STRATEGY] = new Map(); - this[STRATEGY_INSTANCE] = new Map(); - this.closed = false; - } - - /** - * register a custom Schedule Strategy - * @param {String} type - strategy type - * @param {Strategy} clz - Strategy class - */ - use(type, clz) { - this[STRATEGY].set(type, clz); - } - - /** - * load all schedule jobs, then initialize and register speical strategy - */ - init() { - const scheduleItems = loadSchedule(this.agent); - - for (const scheduleItem of Object.values(scheduleItems)) { - this.registerSchedule(scheduleItem); - } - } - - registerSchedule(scheduleItem) { - const { key, schedule } = scheduleItem; - const type = schedule.type; - if (schedule.disable) return; - - // find speical Strategy - const Strategy = this[STRATEGY].get(type); - if (!Strategy) { - const err = new Error(`schedule type [${type}] is not defined`); - err.name = 'EggScheduleError'; - throw err; - } - - // Initialize strategy and register - const instance = new Strategy(schedule, this.agent, key); - this[STRATEGY_INSTANCE].set(key, instance); - } - - unregisterSchedule(key) { - return this[STRATEGY_INSTANCE].delete(key); - } - - /** - * job finish event handler - * - * @param {Object} info - { key, success, message } - */ - onJobFinish(info) { - this.logger.debug(`[Job#${info.id}] ${info.key} finish event received by agent from worker#${info.workerId}`); - - const instance = this[STRATEGY_INSTANCE].get(info.key); - /* istanbul ignore else */ - if (instance) { - instance.onJobFinish(info); - } - } - - /** - * start schedule - */ - start() { - this.closed = false; - for (const instance of this[STRATEGY_INSTANCE].values()) { - instance.start(); - } - } - - close() { - this.closed = true; - } -}; diff --git a/lib/schedule_worker.js b/lib/schedule_worker.js deleted file mode 100644 index 2b00557..0000000 --- a/lib/schedule_worker.js +++ /dev/null @@ -1,23 +0,0 @@ -'use strict'; - -const loadSchedule = require('./load_schedule'); - -module.exports = class ScheduleWorker { - constructor(app) { - this.app = app; - this.scheduleItems = {}; - } - - init() { - this.scheduleItems = loadSchedule(this.app); - } - - registerSchedule(scheduleItem) { - const { key } = scheduleItem; - this.scheduleItems[key] = scheduleItem; - } - - unregisterSchedule(key) { - delete this.scheduleItems[key]; - } -}; diff --git a/lib/strategy/all.js b/lib/strategy/all.js deleted file mode 100644 index 1b7b7df..0000000 --- a/lib/strategy/all.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - -const Strategy = require('./timer'); - -module.exports = class AllStrategy extends Strategy { - handler() { - this.sendAll(); - } -}; diff --git a/lib/strategy/timer.js b/lib/strategy/timer.js deleted file mode 100644 index 5acc73c..0000000 --- a/lib/strategy/timer.js +++ /dev/null @@ -1,98 +0,0 @@ -'use strict'; - -const Strategy = require('./base'); -const parser = require('cron-parser'); -const ms = require('humanize-ms'); -const safetimers = require('safe-timers'); -const assert = require('assert'); -const utility = require('utility'); -const is = require('is-type-of'); -const CRON_INSTANCE = Symbol('cron_instance'); - -module.exports = class TimerStrategy extends Strategy { - constructor(...args) { - super(...args); - - const { interval, cron, cronOptions, immediate } = this.schedule; - assert(interval || cron || immediate, `[egg-schedule] ${this.key} schedule.interval or schedule.cron or schedule.immediate must be present`); - assert(is.function(this.handler), `[egg-schedule] ${this.key} strategy should override \`handler()\` method`); - - // init cron parser - if (cron) { - try { - this[CRON_INSTANCE] = parser.parseExpression(cron, cronOptions); - } catch (err) { - err.message = `[egg-schedule] ${this.key} parse cron instruction(${cron}) error: ${err.message}`; - throw err; - } - } - } - - start() { - /* istanbul ignore next */ - if (this.agent.schedule.closed) return; - - if (this.schedule.immediate) { - this.logger.info(`[Timer] ${this.key} next time will execute immediate`); - setImmediate(() => this.handler()); - } else { - this._scheduleNext(); - } - } - - _scheduleNext() { - /* istanbul ignore next */ - if (this.agent.schedule.closed) return; - - // get next tick - const nextTick = this.getNextTick(); - - if (nextTick) { - this.logger.info(`[Timer] ${this.key} next time will execute after ${nextTick}ms at ${utility.logDate(new Date(Date.now() + nextTick))}`); - this.safeTimeout(() => this.handler(), nextTick); - } else { - this.logger.info(`[Timer] ${this.key} reach endDate, will stop`); - } - } - - onJobStart() { - // Next execution will trigger task at a fix rate, regardless of its execution time. - this._scheduleNext(); - } - - /** - * calculate next tick - * - * @return {Number|undefined} time interval, if out of range then return `undefined` - */ - getNextTick() { - // interval-style - if (this.schedule.interval) return ms(this.schedule.interval); - - // cron-style - if (this[CRON_INSTANCE]) { - // calculate next cron tick - const now = Date.now(); - let nextTick; - let nextInterval; - - // loop to find next feature time - do { - try { - nextInterval = this[CRON_INSTANCE].next(); - nextTick = nextInterval.getTime(); - } catch (err) { - // Error: Out of the timespan range - return; - } - } while (now >= nextTick); - - return nextTick - now; - } - } - - safeTimeout(handler, delay, ...args) { - const fn = delay < safetimers.maxInterval ? setTimeout : safetimers.setTimeout; - return fn(handler, delay, ...args); - } -}; diff --git a/lib/strategy/worker.js b/lib/strategy/worker.js deleted file mode 100644 index 5b83024..0000000 --- a/lib/strategy/worker.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - -const Strategy = require('./timer'); - -module.exports = class WorkerStrategy extends Strategy { - handler() { - this.sendOne(); - } -}; diff --git a/package.json b/package.json index b5de675..c368f6c 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,13 @@ { - "name": "egg-schedule", + "name": "@eggjs/schedule", "version": "4.0.1", + "engines": { + "node": ">=18.19.0" + }, "description": "schedule plugin for egg, support corn job.", "eggPlugin": { "name": "schedule" }, - "files": [ - "app", - "lib", - "config", - "agent.js", - "app.js" - ], "repository": { "type": "git", "url": "git@github.com:eggjs/egg-schedule.git" @@ -24,30 +20,61 @@ "cron" ], "dependencies": { - "cron-parser": "^2.16.3", - "humanize-ms": "^1.2.1", - "is-type-of": "^1.2.1", + "cron-parser": "^4.9.0", + "humanize-ms": "^2.0.0", + "is-type-of": "^2.1.0", "safe-timers": "^1.1.0", - "utility": "^1.16.3" + "utility": "^2.1.0" }, "devDependencies": { - "egg": "^3.7.0", - "egg-bin": "^5.5.0", - "egg-mock": "^5.3.0", - "egg-tracer": "^1.1.0", - "eslint": "^8.29.0", - "eslint-config-egg": "^12.1.0" - }, - "engines": { - "node": ">=14.17.0" + "@arethetypeswrong/cli": "^0.17.1", + "@eggjs/tsconfig": "1", + "@types/mocha": "10", + "@types/node": "22", + "@types/safe-timers": "^1.1.2", + "egg": "beta", + "egg-bin": "6", + "egg-mock": "5", + "egg-tracer": "2", + "eslint": "8", + "eslint-config-egg": "14", + "tshy": "3", + "tshy-after": "1", + "typescript": "5" }, "scripts": { - "lint": "eslint .", - "test": "npm run lint -- --fix && npm run test-local", - "test-local": "egg-bin test", - "cov": "egg-bin cov", - "ci": "npm run lint && npm run cov" + "lint": "eslint --cache src test --ext .ts", + "test": "npm run lint -- --fix && egg-bin test", + "ci": "npm run lint && egg-bin cov && npm run prepublishOnly && attw --pack", + "prepublishOnly": "tshy && tshy-after" }, "author": "dead_horse", - "license": "MIT" + "license": "MIT", + "type": "module", + "tshy": { + "exports": { + ".": "./src/index.ts", + "./package.json": "./package.json" + } + }, + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "types": "./dist/commonjs/index.d.ts", + "default": "./dist/commonjs/index.js" + } + }, + "./package.json": "./package.json" + }, + "files": [ + "dist", + "src" + ], + "types": "./dist/commonjs/index.d.ts", + "main": "./dist/commonjs/index.js", + "module": "./dist/esm/index.js" } diff --git a/src/agent.ts b/src/agent.ts new file mode 100644 index 0000000..f561da2 --- /dev/null +++ b/src/agent.ts @@ -0,0 +1,31 @@ +import type { Agent, ILifecycleBoot } from 'egg'; +import { WorkerStrategy } from './lib/strategy/worker.js'; +import { AllStrategy } from './lib/strategy/all.js'; +import { ScheduleJobInfo } from './lib/types.js'; + +export default class Boot implements ILifecycleBoot { + #agent: Agent; + constructor(agent: Agent) { + this.#agent = agent; + } + + async didLoad(): Promise { + // register built-in strategy + this.#agent.schedule.use('worker', WorkerStrategy); + this.#agent.schedule.use('all', AllStrategy); + + // wait for other plugin to register custom strategy + await this.#agent.schedule.init(); + + // dispatch job finish event to strategy + this.#agent.messenger.on('egg-schedule', (info: ScheduleJobInfo) => { + // get job info from worker + this.#agent.schedule.onJobFinish(info); + }); + + this.#agent.messenger.once('egg-ready', () => { + // start schedule after worker ready + this.#agent.schedule.start(); + }); + } +} diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 0000000..2ef8c66 --- /dev/null +++ b/src/app.ts @@ -0,0 +1,131 @@ +import path from 'node:path'; +import type { + Application, ILifecycleBoot, EggLogger, +} from 'egg'; +import { ScheduleItem, ScheduleJobInfo } from './lib/types.js'; + +export default class Boot implements ILifecycleBoot { + #app: Application; + #logger: EggLogger; + constructor(app: Application) { + this.#app = app; + this.#logger = app.getLogger('scheduleLogger'); + } + + async didLoad(): Promise { + const scheduleWorker = this.#app.scheduleWorker; + await scheduleWorker.init(); + + // log schedule list + for (const s in scheduleWorker.scheduleItems) { + const schedule = scheduleWorker.scheduleItems[s]; + if (!schedule.schedule.disable) { + this.#logger.info('[egg-schedule]: register schedule %s', schedule.key); + } + } + + // register schedule event + this.#app.messenger.on('egg-schedule', async info => { + const { id, key } = info; + this.#logger.debug(`[Job#${id}] ${key} await app ready`); + await this.#app.ready(); + const schedule = scheduleWorker.scheduleItems[key]; + this.#logger.debug(`[Job#${id}] ${key} task received by app`); + + if (!schedule) { + this.#logger.warn(`[Job#${id}] ${key} unknown task`); + return; + } + + /* istanbul ignore next */ + if (schedule.schedule.disable) { + this.#logger.warn(`[Job#${id}] ${key} disable`); + return; + } + + this.#logger.info(`[Job#${id}] ${key} executing by app`); + + // run with anonymous context + const ctx = this.#app.createAnonymousContext({ + method: 'SCHEDULE', + url: `/__schedule?path=${key}&${schedule.scheduleQueryString}`, + }); + + const start = Date.now(); + + let success: boolean; + let e: Error | undefined; + try { + // execute + await this.#app.ctxStorage.run(ctx, async () => { + return await schedule.task(ctx, ...info.args); + }); + success = true; + } catch (err) { + success = false; + throw err; + } + + const rt = Date.now() - start; + + const msg = `[Job#${id}] ${key} execute ${success ? 'succeed' : 'failed'}, used ${rt}ms.`; + if (success) { + this.#logger.info(msg); + } else { + this.#logger.error(msg, e); + } + + // notify agent job finish + this.#app.messenger.sendToAgent('egg-schedule', { + ...info, + success, + workerId: process.pid, + rt, + message: e?.message, + } as ScheduleJobInfo); + }); + + // for test purpose + const config = this.#app.config; + const directory = [ + path.join(config.baseDir, 'app/schedule'), + ...config.schedule.directory, + ]; + const runSchedule = async (schedulePath: string, ...args: any[]) => { + // resolve real path + if (path.isAbsolute(schedulePath)) { + schedulePath = require.resolve(schedulePath); + } else { + for (const dir of directory) { + try { + schedulePath = require.resolve(path.join(dir, schedulePath)); + break; + } catch (_) { + /* istanbul ignore next */ + } + } + } + + let schedule: ScheduleItem; + try { + schedule = scheduleWorker.scheduleItems[schedulePath]; + if (!schedule) { + throw new Error(`Cannot find schedule ${schedulePath}`); + } + } catch (err: any) { + err.message = `[egg-schedule] ${err.message}`; + throw err; + } + + // run with anonymous context + const ctx = this.#app.createAnonymousContext({ + method: 'SCHEDULE', + url: `/__schedule?path=${schedulePath}&${schedule.scheduleQueryString}`, + }); + return await this.#app.ctxStorage.run(ctx, async () => { + return await schedule.task(ctx, ...args); + }); + }; + Reflect.set(this.#app, 'runSchedule', runSchedule); + } +} diff --git a/app/extend/agent.js b/src/app/extend/agent.ts similarity index 65% rename from app/extend/agent.js rename to src/app/extend/agent.ts index 907c66d..e519689 100644 --- a/app/extend/agent.js +++ b/src/app/extend/agent.ts @@ -1,16 +1,14 @@ -'use strict'; - -const Strategy = require('../../lib/strategy/base'); -const TimerStrategy = require('../../lib/strategy/timer'); -const Schedule = require('../../lib/schedule'); +import { BaseStrategy } from '../../lib/strategy/base.js'; +import { TimerStrategy } from '../../lib/strategy/timer.js'; +import { Schedule } from '../../lib/schedule.js'; const SCHEDULE = Symbol('agent#schedule'); -module.exports = { +export default { /** * @member agent#ScheduleStrategy */ - ScheduleStrategy: Strategy, + ScheduleStrategy: BaseStrategy, /** * @member agent#TimerScheduleStrategy @@ -29,4 +27,4 @@ module.exports = { } return this[SCHEDULE]; }, -}; +} as any; diff --git a/app/extend/application.js b/src/app/extend/application.ts similarity index 65% rename from app/extend/application.js rename to src/app/extend/application.ts index fad95b4..0003596 100644 --- a/app/extend/application.js +++ b/src/app/extend/application.ts @@ -1,12 +1,10 @@ -'use strict'; - -const ScheduleWorker = require('../../lib/schedule_worker'); +import { ScheduleWorker } from '../../lib/schedule_worker.js'; const SCHEDULE_WORKER = Symbol('application#scheduleWorker'); -module.exports = { +export default { /** - * @member agent#schedule + * @member app#schedule */ get scheduleWorker() { if (!this[SCHEDULE_WORKER]) { @@ -14,4 +12,5 @@ module.exports = { } return this[SCHEDULE_WORKER]; }, -}; +} as any; + diff --git a/config/config.default.js b/src/config/config.default.ts similarity index 77% rename from config/config.default.js rename to src/config/config.default.ts index 0fba4d8..18de77e 100644 --- a/config/config.default.js +++ b/src/config/config.default.ts @@ -1,7 +1,5 @@ -'use strict'; - -module.exports = () => { - const config = {}; +export default () => { + const config = {} as Record; config.customLogger = { scheduleLogger: { diff --git a/src/lib/load_schedule.ts b/src/lib/load_schedule.ts new file mode 100644 index 0000000..0f011f6 --- /dev/null +++ b/src/lib/load_schedule.ts @@ -0,0 +1,69 @@ +import path from 'node:path'; +import assert from 'node:assert'; +import { stringify } from 'node:querystring'; +import { isClass, isFunction } from 'is-type-of'; +import type { EggApplicationCore, EggContext } from 'egg'; +import type { ScheduleConfig, ScheduleTask, ScheduleItem } from './types.js'; + +function getScheduleLoader(app: EggApplicationCore) { + return class ScheduleLoader extends app.loader.FileLoader { + async load() { + const target = this.options.target as Record; + const items = await this.parse(); + for (const item of items) { + const schedule = item.exports as { schedule: ScheduleConfig, task: ScheduleTask }; + const fullpath = item.fullpath; + const scheduleConfig = schedule.schedule; + assert(scheduleConfig, `schedule(${fullpath}): must have "schedule" and "task" properties`); + assert(isClass(schedule) || isFunction(schedule.task), + `schedule(${fullpath}: \`schedule.task\` should be function or \`schedule\` should be class`); + + let task: ScheduleTask; + if (isClass(schedule)) { + task = async (ctx: EggContext, ...args: any[]) => { + const instance = new schedule(ctx); + // s.subscribe = app.toAsyncFunction(s.subscribe); + return instance.subscribe(...args); + }; + } else { + task = schedule.task; + // task = app.toAsyncFunction(schedule.task); + } + + const env = app.config.env; + const envList = schedule.schedule.env; + if (Array.isArray(envList) && !envList.includes(env)) { + app.coreLogger.info(`[egg-schedule]: ignore schedule ${fullpath} due to \`schedule.env\` not match`); + continue; + } + + // handle symlink case + const realFullpath = require.resolve(fullpath); + target[realFullpath] = { + schedule: scheduleConfig, + scheduleQueryString: stringify(scheduleConfig as any), + task, + key: realFullpath, + }; + } + return target; + } + }; +} + +export async function loadSchedule(app: EggApplicationCore) { + const dirs = [ + app.loader.getLoadUnits().map(unit => path.join(unit.path, 'app/schedule')), + ...app.config.schedule.directory, + ]; + + const Loader = getScheduleLoader(app); + const schedules = {} as Record; + await new Loader({ + directory: dirs, + target: schedules, + inject: app, + }).load(); + Reflect.set(app, 'schedules', schedules); + return schedules; +} diff --git a/src/lib/schedule.ts b/src/lib/schedule.ts new file mode 100644 index 0000000..9d3df96 --- /dev/null +++ b/src/lib/schedule.ts @@ -0,0 +1,90 @@ +import type { Agent, EggLogger } from 'egg'; +import { loadSchedule } from './load_schedule.js'; +import type { ScheduleItem, ScheduleJobInfo } from './types.js'; +import type { BaseStrategy } from './strategy/base.js'; + +export class Schedule { + closed = false; + + #agent: Agent; + #logger: EggLogger; + #strategyClassMap = new Map(); + #strategyInstanceMap = new Map(); + + constructor(agent: Agent) { + this.#agent = agent; + this.#logger = agent.getLogger('scheduleLogger'); + } + + /** + * register a custom Schedule Strategy + * @param {String} type - strategy type + * @param {Strategy} clz - Strategy class + */ + use(type: string, clz: typeof BaseStrategy) { + this.#strategyClassMap.set(type, clz); + } + + /** + * load all schedule jobs, then initialize and register speical strategy + */ + async init() { + const scheduleItems = await loadSchedule(this.#agent); + for (const scheduleItem of Object.values(scheduleItems)) { + this.registerSchedule(scheduleItem); + } + } + + registerSchedule(scheduleItem: ScheduleItem) { + const { key, schedule } = scheduleItem; + const type = schedule.type; + if (schedule.disable) return; + + // find Strategy by type + const Strategy = this.#strategyClassMap.get(type!); + if (!Strategy) { + const err = new Error(`schedule type [${type}] is not defined`); + err.name = 'EggScheduleError'; + throw err; + } + + // Initialize strategy and register + const instance = new Strategy(schedule, this.#agent, key); + this.#strategyInstanceMap.set(key, instance); + } + + unregisterSchedule(key: string) { + return this.#strategyInstanceMap.delete(key); + } + + /** + * job finish event handler + * + * @param {Object} info - { id, key, success, message, workerId } + */ + onJobFinish(info: ScheduleJobInfo) { + this.#logger.debug(`[Job#${info.id}] ${info.key} finish event received by agent from worker#${info.workerId}`); + const instance = this.#strategyInstanceMap.get(info.key); + /* istanbul ignore else */ + if (instance) { + instance.onJobFinish(info); + } + } + + /** + * start schedule + */ + start() { + this.closed = false; + for (const instance of this.#strategyInstanceMap.values()) { + instance.start(); + } + } + + close() { + this.closed = true; + for (const instance of this.#strategyInstanceMap.values()) { + instance.close(); + } + } +} diff --git a/src/lib/schedule_worker.ts b/src/lib/schedule_worker.ts new file mode 100644 index 0000000..12bd8af --- /dev/null +++ b/src/lib/schedule_worker.ts @@ -0,0 +1,24 @@ +import type { Application } from 'egg'; +import { loadSchedule } from './load_schedule.js'; +import type { ScheduleItem } from './types.js'; + +export class ScheduleWorker { + #app: Application; + scheduleItems: Record; + + constructor(app: Application) { + this.#app = app; + } + + async init() { + this.scheduleItems = await loadSchedule(this.#app); + } + + registerSchedule(scheduleItem: ScheduleItem) { + this.scheduleItems[scheduleItem.key] = scheduleItem; + } + + unregisterSchedule(key: string) { + delete this.scheduleItems[key]; + } +} diff --git a/src/lib/strategy/all.ts b/src/lib/strategy/all.ts new file mode 100644 index 0000000..5bd033b --- /dev/null +++ b/src/lib/strategy/all.ts @@ -0,0 +1,7 @@ +import { TimerStrategy } from './timer.js'; + +export class AllStrategy extends TimerStrategy { + handler() { + this.sendAll(); + } +} diff --git a/lib/strategy/base.js b/src/lib/strategy/base.ts similarity index 56% rename from lib/strategy/base.js rename to src/lib/strategy/base.ts index b37af18..19b5832 100644 --- a/lib/strategy/base.js +++ b/src/lib/strategy/base.ts @@ -1,26 +1,41 @@ -'use strict'; +import type { Agent, EggLogger } from 'egg'; +import type { ScheduleConfig, ScheduleJobInfo } from '../types.js'; -module.exports = class BaseStrategy { - constructor(schedule, agent, key) { +export class BaseStrategy { + protected agent: Agent; + protected scheduleConfig: ScheduleConfig; + protected key: string; + protected logger: EggLogger; + protected closed = false; + count = 0; + + constructor(scheduleConfig: ScheduleConfig, agent: Agent, key: string) { this.agent = agent; this.key = key; - this.schedule = schedule; + this.scheduleConfig = scheduleConfig; this.logger = this.agent.getLogger('scheduleLogger'); - this.count = 0; } - start() {} + start() { + throw new TypeError(`[egg-schedule] ${this.key} strategy should override \`start()\` method`); + } + + close() { + this.closed = true; + } - onJobStart() {} + // eslint-disable-next-line @typescript-eslint/no-unused-vars + onJobStart(_info: ScheduleJobInfo) {} - onJobFinish() {} + // eslint-disable-next-line @typescript-eslint/no-unused-vars + onJobFinish(_info: ScheduleJobInfo) {} /** * trigger one worker * * @param {...any} args - pass to job task */ - sendOne(...args) { + sendOne(...args: any[]) { /* istanbul ignore next */ if (this.agent.schedule.closed) { this.logger.warn(`${this.key} skip due to schedule closed`); @@ -33,7 +48,7 @@ module.exports = class BaseStrategy { key: this.key, id: this.getSeqId(), args, - }; + } as ScheduleJobInfo; this.logger.debug(`[Job#${info.id}] ${info.key} triggered, send random by agent`); this.agent.messenger.sendRandom('egg-schedule', info); @@ -45,7 +60,7 @@ module.exports = class BaseStrategy { * * @param {...any} args - pass to job task */ - sendAll(...args) { + sendAll(...args: any[]) { /* istanbul ignore next */ if (this.agent.schedule.closed) { this.logger.warn(`${this.key} skip due to schedule closed`); @@ -58,8 +73,9 @@ module.exports = class BaseStrategy { key: this.key, id: this.getSeqId(), args, - }; + } as ScheduleJobInfo; this.logger.debug(`[Job#${info.id}] ${info.key} triggered, send all by agent`); + // send to all workers this.agent.messenger.send('egg-schedule', info); this.onJobStart(info); } @@ -67,4 +83,4 @@ module.exports = class BaseStrategy { getSeqId() { return `${Date.now()}${process.hrtime().join('')}${this.count}`; } -}; +} diff --git a/src/lib/strategy/timer.ts b/src/lib/strategy/timer.ts new file mode 100644 index 0000000..34f400c --- /dev/null +++ b/src/lib/strategy/timer.ts @@ -0,0 +1,106 @@ +import assert from 'node:assert'; +import { parseExpression, type CronExpression } from 'cron-parser'; +import { ms } from 'humanize-ms'; +import safeTimers from 'safe-timers'; +import { logDate } from 'utility'; +import type { Agent } from 'egg'; +import type { ScheduleConfig } from '../types.js'; +import { BaseStrategy } from './base.js'; + +export abstract class TimerStrategy extends BaseStrategy { + protected cronInstance?: CronExpression; + + constructor(scheduleConfig: ScheduleConfig, agent: Agent, key: string) { + super(scheduleConfig, agent, key); + + const { interval, cron, cronOptions, immediate } = this.scheduleConfig; + assert(interval || cron || immediate, + `[egg-schedule] ${this.key} \`schedule.interval\` or \`schedule.cron]\` or \`schedule.immediate\` must be present`); + + // init cron parser + if (cron) { + try { + this.cronInstance = parseExpression(cron, cronOptions); + } catch (err: any) { + throw new TypeError( + `[egg-schedule] ${this.key} parse cron instruction(${cron}) error: ${err.message}`, + { cause: err }); + } + } + } + + protected handler() { + throw new TypeError(`[egg-schedule] ${this.key} strategy should override \`handler()\` method`); + } + + + start() { + /* istanbul ignore next */ + if (this.agent.schedule.closed) return; + + if (this.scheduleConfig.immediate) { + this.logger.info(`[Timer] ${this.key} next time will execute immediate`); + setImmediate(() => this.handler()); + } else { + this.#scheduleNext(); + } + } + + #scheduleNext() { + /* istanbul ignore next */ + if (this.agent.schedule.closed) return; + + // get next tick + const nextTick = this.getNextTick(); + if (nextTick) { + this.logger.info( + `[Timer] ${this.key} next time will execute after ${nextTick}ms at ${logDate(new Date(Date.now() + nextTick))}`); + this.safeTimeout(() => this.handler(), nextTick); + } else { + this.logger.info(`[Timer] ${this.key} reach endDate, will stop`); + } + } + + onJobStart() { + // Next execution will trigger task at a fix rate, regardless of its execution time. + this.#scheduleNext(); + } + + /** + * calculate next tick + * + * @return {Number|undefined} time interval, if out of range then return `undefined` + */ + protected getNextTick(): number | undefined { + // interval-style + if (this.scheduleConfig.interval) { + return ms(this.scheduleConfig.interval); + } + + // cron-style + if (this.cronInstance) { + // calculate next cron tick + const now = Date.now(); + let nextTick: number; + + // loop to find next feature time + do { + try { + const nextInterval = this.cronInstance.next(); + nextTick = nextInterval.getTime(); + } catch (err) { + // Error: Out of the timespan range + this.logger.info(`[Timer] ${this.key} cron out of the timespan range, error: %s`, err); + return; + } + } while (now >= nextTick); + return nextTick - now; + } + // won\'t run here + } + + protected safeTimeout(handler: () => void, delay: number, ...args: any[]) { + const fn = delay < safeTimers.maxInterval ? setTimeout : safeTimers.setTimeout; + return fn(handler, delay, ...args); + } +} diff --git a/src/lib/strategy/worker.ts b/src/lib/strategy/worker.ts new file mode 100644 index 0000000..078dbe1 --- /dev/null +++ b/src/lib/strategy/worker.ts @@ -0,0 +1,7 @@ +import { TimerStrategy } from './timer.js'; + +export class WorkerStrategy extends TimerStrategy { + handler() { + this.sendOne(); + } +} diff --git a/src/lib/types.ts b/src/lib/types.ts new file mode 100644 index 0000000..4321e26 --- /dev/null +++ b/src/lib/types.ts @@ -0,0 +1,50 @@ +import type { ParserOptions as CronOptions } from 'cron-parser'; +import type { Schedule } from './schedule.js'; +import type { ScheduleWorker } from './schedule_worker.js'; + +/** + * Schedule Config + * @see https://www.eggjs.org/zh-CN/basics/schedule + */ +export interface ScheduleConfig { + type?: 'worker' | 'all'; + interval?: string | number; + cron?: string; + cronOptions?: CronOptions; + immediate?: boolean; + disable?: boolean; + env?: string[]; +} + +export type ScheduleTask = (ctx: any, ...args: any[]) => Promise; + +export interface ScheduleItem { + schedule: ScheduleConfig; + scheduleQueryString: string; + task: ScheduleTask; + key: string; +} + +export interface ScheduleJobInfo { + id: string; + key: string; + workerId: number; + args: any[]; + success?: boolean; + message?: string; + rt?: number; +} + +declare module 'egg' { + export interface ScheduleAgent { + schedule: Schedule; + } + export interface Agent extends ScheduleAgent {} + + export interface ScheduleApplication { + scheduleWorker: ScheduleWorker; + /** runSchedule in unittest */ + runSchedule: (schedulePath: string, ...args: any[]) => Promise; + } + export interface Application extends ScheduleApplication {} +} diff --git a/test/schedule.test.js b/test/schedule.test.ts similarity index 98% rename from test/schedule.test.js rename to test/schedule.test.ts index 865ad0b..a416007 100644 --- a/test/schedule.test.js +++ b/test/schedule.test.ts @@ -1,17 +1,12 @@ -const mm = require('egg-mock'); -const path = require('path'); -const fs = require('fs'); -const assert = require('assert'); -const is = require('is-type-of'); - -function sleep(ms) { - return new Promise(resolve => { - setTimeout(resolve, ms); - }); -} - -describe('test/schedule.test.js', () => { - let app; +import { strict as assert } from 'node:assert'; +import path from 'node:path'; +import fs from 'node:fs'; +import { setTimeout as sleep } from 'node:timers/promises'; +import is from 'is-type-of'; +import mm, { MockApplication } from 'egg-mock'; + +describe('test/schedule.test.ts', () => { + let app: MockApplication; afterEach(() => app.close()); describe('schedule type worker', () => { diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ff41b73 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@eggjs/tsconfig", + "compilerOptions": { + "strict": true, + "noImplicitAny": true, + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext" + } +} From 2e7079a496a2a46d74b32727e69c8df47f420665 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Thu, 12 Dec 2024 22:16:14 +0800 Subject: [PATCH 02/10] f --- package.json | 2 +- test/schedule.test.ts | 64 ++++++++++++++++++++++--------------------- 2 files changed, 34 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index c368f6c..f2c6f38 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@types/safe-timers": "^1.1.2", "egg": "beta", "egg-bin": "6", - "egg-mock": "5", + "egg-mock": "^5.15.1", "egg-tracer": "2", "eslint": "8", "eslint-config-egg": "14", diff --git a/test/schedule.test.ts b/test/schedule.test.ts index a416007..34630de 100644 --- a/test/schedule.test.ts +++ b/test/schedule.test.ts @@ -2,15 +2,17 @@ import { strict as assert } from 'node:assert'; import path from 'node:path'; import fs from 'node:fs'; import { setTimeout as sleep } from 'node:timers/promises'; -import is from 'is-type-of'; -import mm, { MockApplication } from 'egg-mock'; +import { MockApplication } from 'egg-mock'; +import _mm from 'egg-mock'; + +const mm = _mm.default; describe('test/schedule.test.ts', () => { let app: MockApplication; afterEach(() => app.close()); describe('schedule type worker', () => { - it('should support interval and cron', async () => { + it.only('should support interval and cron', async () => { app = mm.cluster({ baseDir: 'worker', workers: 2, cache: false }); // app.debug(); await app.ready(); @@ -183,8 +185,8 @@ describe('test/schedule.test.ts', () => { // app.debug(); await app.ready(); await sleep(1000); - app.expect('code', 1); - app.expect('stderr', /should provide clusterId/); + // app.expect('code', 1); + // app.expect('stderr', /should provide clusterId/); }); }); @@ -194,7 +196,7 @@ describe('test/schedule.test.ts', () => { // app.debug(); await app.ready(); await sleep(3000); - app.expect('stderr', /schedule\.interval or schedule\.cron or schedule\.immediate must be present/); + // app.expect('stderr', /schedule\.interval or schedule\.cron or schedule\.immediate must be present/); }); }); @@ -203,7 +205,7 @@ describe('test/schedule.test.ts', () => { app = mm.cluster({ baseDir: 'typeUndefined', workers: 2 }); await app.ready(); await sleep(3000); - app.expect('stderr', /schedule type \[undefined\] is not defined/); + // app.expect('stderr', /schedule type \[undefined\] is not defined/); }); }); @@ -213,7 +215,7 @@ describe('test/schedule.test.ts', () => { // app.debug(); await app.ready(); await sleep(1000); - app.expect('stderr', /parse cron instruction\(invalid instruction\) error/); + // app.expect('stderr', /parse cron instruction\(invalid instruction\) error/); }); }); @@ -255,13 +257,13 @@ describe('test/schedule.test.ts', () => { interval: 4000, }, }; - app.agent.schedule.registerSchedule(schedule); - app.scheduleWorker.registerSchedule(schedule); + // app.agent.schedule.registerSchedule(schedule); + app.scheduleWorker.registerSchedule(schedule as any); await app.runSchedule(key); await sleep(1000); - assert(scheduleCalled === true); + assert.equal(scheduleCalled, true); }); it('should unregister succeed', async () => { @@ -280,20 +282,20 @@ describe('test/schedule.test.ts', () => { interval: 4000, }, }; - app.agent.schedule.registerSchedule(schedule); - app.scheduleWorker.registerSchedule(schedule); + // app.agent.schedule.registerSchedule(schedule); + app.scheduleWorker.registerSchedule(schedule as any); - app.agent.schedule.unregisterSchedule(schedule.key); + // app.agent.schedule.unregisterSchedule(schedule.key); app.scheduleWorker.unregisterSchedule(schedule.key); - let err; + let err: any; try { await app.runSchedule(key); } catch (e) { err = e; } - assert(err.message.includes('Cannot find schedule')); - assert(scheduleCalled === false); + assert.match(err.message, /Cannot find schedule/); + assert.equal(scheduleCalled, false); }); }); @@ -305,7 +307,7 @@ describe('test/schedule.test.ts', () => { await app.runSchedule(__filename); await sleep(1000); throw new Error('should not execute'); - } catch (err) { + } catch (err: any) { assert(err.message.includes('Cannot find schedule')); } }); @@ -448,7 +450,8 @@ describe('test/schedule.test.ts', () => { it('should export app.schedules', async () => { app = mm.app({ baseDir: 'worker', cache: false }); await app.ready(); - assert(app.schedules); + assert('schedules' in app); + assert(Reflect.get(app, 'schedules')); }); }); @@ -550,7 +553,7 @@ describe('test/schedule.test.ts', () => { describe('detect error', () => { it('should works', async () => { app = mm.cluster({ baseDir: 'detect-error', workers: 1, cache: false }); - app.debug(); + // app.debug(); await app.ready(); await sleep(2000); @@ -562,34 +565,33 @@ describe('test/schedule.test.ts', () => { }); }); -function getCoreLogContent(name) { +function getCoreLogContent(name: string) { const logPath = path.join(__dirname, 'fixtures', name, 'logs', name, 'egg-web.log'); return fs.readFileSync(logPath, 'utf8'); } -function getLogContent(name) { +function getLogContent(name: string) { const logPath = path.join(__dirname, 'fixtures', name, 'logs', name, `${name}-web.log`); return fs.readFileSync(logPath, 'utf8'); } -/* eslint-disable-next-line no-unused-vars */ -function getErrorLogContent(name) { - const logPath = path.join(__dirname, 'fixtures', name, 'logs', name, 'common-error.log'); - return fs.readFileSync(logPath, 'utf8'); -} +// function getErrorLogContent(name) { +// const logPath = path.join(__dirname, 'fixtures', name, 'logs', name, 'common-error.log'); +// return fs.readFileSync(logPath, 'utf8'); +// } -function getAgentLogContent(name) { +function getAgentLogContent(name: string) { const logPath = path.join(__dirname, 'fixtures', name, 'logs', name, 'egg-agent.log'); return fs.readFileSync(logPath, 'utf8'); } -function getScheduleLogContent(name) { +function getScheduleLogContent(name: string) { const logPath = path.join(__dirname, 'fixtures', name, 'logs', name, 'egg-schedule.log'); return fs.readFileSync(logPath, 'utf8'); } -function contains(content, match) { +function contains(content: string, match: string | RegExp) { return content.split('\n').filter(line => { - return is.regexp(match) ? match.test(line) : line.indexOf(match) >= 0; + return match instanceof RegExp ? match.test(line) : line.indexOf(match) >= 0; }).length; } From 7208139816fddc0bcb7a142ed928581e30cf3bc3 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Tue, 17 Dec 2024 15:24:32 +0800 Subject: [PATCH 03/10] fix test --- .github/workflows/nodejs.yml | 2 + README.md | 3 +- package.json | 14 ++++-- src/agent.ts | 4 ++ src/app.ts | 5 ++ src/app/extend/agent.ts | 2 +- src/lib/load_schedule.ts | 2 +- src/lib/strategy/base.ts | 11 +++-- test/fixtures/customType/agent.js | 20 ++++---- test/fixtures/customTypeWithoutStart/agent.js | 2 - test/fixtures/plugin/config/config.default.js | 3 ++ test/fixtures/worker/app/schedule/interval.js | 2 - test/fixtures/worker/app/schedule/sub/cron.js | 2 - test/fixtures/worker/config/config.default.js | 8 ++++ test/fixtures/worker/config/plugin.js | 12 ++++- test/schedule.test.ts | 46 +++++++++---------- 16 files changed, 86 insertions(+), 52 deletions(-) create mode 100644 test/fixtures/plugin/config/config.default.js create mode 100644 test/fixtures/worker/config/config.default.js diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index dc5563d..5a2a289 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -13,3 +13,5 @@ jobs: with: os: 'ubuntu-latest' version: '18, 20, 22' + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/README.md b/README.md index b680ed9..37ee3d2 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,13 @@ [![Test coverage][codecov-image]][codecov-url] [![Known Vulnerabilities][snyk-image]][snyk-url] [![npm download][download-image]][download-url] +[![Node.js Version](https://img.shields.io/node/v/@eggjs/schedule.svg?style=flat)](https://nodejs.org/en/download/) [npm-image]: https://img.shields.io/npm/v/@eggjs/schedule.svg?style=flat-square [npm-url]: https://npmjs.org/package/@eggjs/schedule [codecov-image]: https://codecov.io/github/eggjs/egg-schedule/coverage.svg?branch=master [codecov-url]: https://codecov.io/github/eggjs/egg-schedule?branch=master -[snyk-image]: https://snyk.io/test/npm/egg-schedule/badge.svg?style=flat-square +[snyk-image]: https://snyk.io/test/npm/@eggjs/schedule/badge.svg?style=flat-square [snyk-url]: https://snyk.io/test/npm/@eggjs/schedule [download-image]: https://img.shields.io/npm/dm/@eggjs/schedule.svg?style=flat-square [download-url]: https://npmjs.org/package/@eggjs/schedule diff --git a/package.json b/package.json index f2c6f38..939ab6d 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,11 @@ }, "description": "schedule plugin for egg, support corn job.", "eggPlugin": { - "name": "schedule" + "name": "schedule", + "exports": { + "import": "./dist/esm", + "require": "./dist/commonjs" + } }, "repository": { "type": "git", @@ -34,7 +38,7 @@ "@types/safe-timers": "^1.1.2", "egg": "beta", "egg-bin": "6", - "egg-mock": "^5.15.1", + "egg-mock": "beta", "egg-tracer": "2", "eslint": "8", "eslint-config-egg": "14", @@ -44,8 +48,10 @@ }, "scripts": { "lint": "eslint --cache src test --ext .ts", - "test": "npm run lint -- --fix && egg-bin test", - "ci": "npm run lint && egg-bin cov && npm run prepublishOnly && attw --pack", + "pretest": "npm run lint -- --fix && npm run prepublishOnly", + "test": "egg-bin test", + "preci": "npm run lint && npm run prepublishOnly", + "ci": "egg-bin cov && attw --pack", "prepublishOnly": "tshy && tshy-after" }, "author": "dead_horse", diff --git a/src/agent.ts b/src/agent.ts index f561da2..a3e01e3 100644 --- a/src/agent.ts +++ b/src/agent.ts @@ -1,8 +1,11 @@ +import { debuglog } from 'node:util'; import type { Agent, ILifecycleBoot } from 'egg'; import { WorkerStrategy } from './lib/strategy/worker.js'; import { AllStrategy } from './lib/strategy/all.js'; import { ScheduleJobInfo } from './lib/types.js'; +const debug = debuglog('@eggjs/schedule/agent'); + export default class Boot implements ILifecycleBoot { #agent: Agent; constructor(agent: Agent) { @@ -26,6 +29,7 @@ export default class Boot implements ILifecycleBoot { this.#agent.messenger.once('egg-ready', () => { // start schedule after worker ready this.#agent.schedule.start(); + debug('got egg-ready event, schedule start'); }); } } diff --git a/src/app.ts b/src/app.ts index 2ef8c66..d2a3c77 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,9 +1,12 @@ +import { debuglog } from 'node:util'; import path from 'node:path'; import type { Application, ILifecycleBoot, EggLogger, } from 'egg'; import { ScheduleItem, ScheduleJobInfo } from './lib/types.js'; +const debug = debuglog('@eggjs/schedule/app'); + export default class Boot implements ILifecycleBoot { #app: Application; #logger: EggLogger; @@ -13,6 +16,7 @@ export default class Boot implements ILifecycleBoot { } async didLoad(): Promise { + debug('didLoad'); const scheduleWorker = this.#app.scheduleWorker; await scheduleWorker.init(); @@ -26,6 +30,7 @@ export default class Boot implements ILifecycleBoot { // register schedule event this.#app.messenger.on('egg-schedule', async info => { + debug('app got "egg-schedule" message: %o', info); const { id, key } = info; this.#logger.debug(`[Job#${id}] ${key} await app ready`); await this.#app.ready(); diff --git a/src/app/extend/agent.ts b/src/app/extend/agent.ts index e519689..81dd508 100644 --- a/src/app/extend/agent.ts +++ b/src/app/extend/agent.ts @@ -21,7 +21,7 @@ export default { get schedule() { if (!this[SCHEDULE]) { this[SCHEDULE] = new Schedule(this); - this.beforeClose(() => { + this.lifecycle.registerBeforeClose(() => { return this[SCHEDULE].close(); }); } diff --git a/src/lib/load_schedule.ts b/src/lib/load_schedule.ts index 0f011f6..160515d 100644 --- a/src/lib/load_schedule.ts +++ b/src/lib/load_schedule.ts @@ -53,7 +53,7 @@ function getScheduleLoader(app: EggApplicationCore) { export async function loadSchedule(app: EggApplicationCore) { const dirs = [ - app.loader.getLoadUnits().map(unit => path.join(unit.path, 'app/schedule')), + ...app.loader.getLoadUnits().map(unit => path.join(unit.path, 'app/schedule')), ...app.config.schedule.directory, ]; diff --git a/src/lib/strategy/base.ts b/src/lib/strategy/base.ts index 19b5832..5b48b23 100644 --- a/src/lib/strategy/base.ts +++ b/src/lib/strategy/base.ts @@ -16,8 +16,13 @@ export class BaseStrategy { this.logger = this.agent.getLogger('scheduleLogger'); } + /** keep compatibility */ + get schedule(): ScheduleConfig { + return this.scheduleConfig; + } + start() { - throw new TypeError(`[egg-schedule] ${this.key} strategy should override \`start()\` method`); + // empty loop by default } close() { @@ -50,7 +55,7 @@ export class BaseStrategy { args, } as ScheduleJobInfo; - this.logger.debug(`[Job#${info.id}] ${info.key} triggered, send random by agent`); + this.logger.info(`[Job#${info.id}] ${info.key} triggered, send random by agent`); this.agent.messenger.sendRandom('egg-schedule', info); this.onJobStart(info); } @@ -74,7 +79,7 @@ export class BaseStrategy { id: this.getSeqId(), args, } as ScheduleJobInfo; - this.logger.debug(`[Job#${info.id}] ${info.key} triggered, send all by agent`); + this.logger.info(`[Job#${info.id}] ${info.key} triggered, send all by agent`); // send to all workers this.agent.messenger.send('egg-schedule', info); this.onJobStart(info); diff --git a/test/fixtures/customType/agent.js b/test/fixtures/customType/agent.js index 73e8604..49a5025 100644 --- a/test/fixtures/customType/agent.js +++ b/test/fixtures/customType/agent.js @@ -1,12 +1,12 @@ -'use strict'; - -module.exports = function(agent) { - class ClusterStrategy extends agent.ScheduleStrategy { - start() { - this.interval = setInterval(() => { - this.sendOne(); - }, this.schedule.interval); +module.exports = class Boot { + constructor(agent) { + class ClusterStrategy extends agent.ScheduleStrategy { + start() { + this.interval = setInterval(() => { + this.sendOne(); + }, this.schedule.interval); + } } + agent.schedule.use('cluster', ClusterStrategy); } - agent.schedule.use('cluster', ClusterStrategy); -}; +} diff --git a/test/fixtures/customTypeWithoutStart/agent.js b/test/fixtures/customTypeWithoutStart/agent.js index 316b287..ce87a08 100644 --- a/test/fixtures/customTypeWithoutStart/agent.js +++ b/test/fixtures/customTypeWithoutStart/agent.js @@ -1,5 +1,3 @@ -'use strict'; - module.exports = function(agent) { class ClusterStrategy extends agent.ScheduleStrategy { constructor(...args) { diff --git a/test/fixtures/plugin/config/config.default.js b/test/fixtures/plugin/config/config.default.js new file mode 100644 index 0000000..fbd44f6 --- /dev/null +++ b/test/fixtures/plugin/config/config.default.js @@ -0,0 +1,3 @@ +exports.logger = { + level: 'debug', +}; diff --git a/test/fixtures/worker/app/schedule/interval.js b/test/fixtures/worker/app/schedule/interval.js index 2157971..29efc16 100644 --- a/test/fixtures/worker/app/schedule/interval.js +++ b/test/fixtures/worker/app/schedule/interval.js @@ -1,5 +1,3 @@ -'use strict'; - exports.schedule = { type: 'worker', interval: '4s', diff --git a/test/fixtures/worker/app/schedule/sub/cron.js b/test/fixtures/worker/app/schedule/sub/cron.js index 282d36b..6b3df7b 100644 --- a/test/fixtures/worker/app/schedule/sub/cron.js +++ b/test/fixtures/worker/app/schedule/sub/cron.js @@ -1,5 +1,3 @@ -'use strict'; - exports.schedule = { type: 'worker', cron: '*/5 * * * * *', diff --git a/test/fixtures/worker/config/config.default.js b/test/fixtures/worker/config/config.default.js new file mode 100644 index 0000000..8b3e0dd --- /dev/null +++ b/test/fixtures/worker/config/config.default.js @@ -0,0 +1,8 @@ +exports.logger = { + level: 'DEBUG', + consoleLevel: 'DEBUG', + coreLogger: { + level: 'DEBUG', + consoleLevel: 'DEBUG', + }, +}; diff --git a/test/fixtures/worker/config/plugin.js b/test/fixtures/worker/config/plugin.js index e54d182..08a0b9c 100644 --- a/test/fixtures/worker/config/plugin.js +++ b/test/fixtures/worker/config/plugin.js @@ -1,3 +1,11 @@ -'use strict'; - exports.logrotator = true; +exports.onerror = false; +exports.session = false; +exports.i18n = false; +exports.watcher = false; +exports.multipart = false; +exports.security = false; +exports.development = false; +exports.static = false; +exports.jsonp = false; +exports.view = false; diff --git a/test/schedule.test.ts b/test/schedule.test.ts index 34630de..d343a4e 100644 --- a/test/schedule.test.ts +++ b/test/schedule.test.ts @@ -1,11 +1,13 @@ import { strict as assert } from 'node:assert'; import path from 'node:path'; import fs from 'node:fs'; +import { fileURLToPath } from 'node:url'; import { setTimeout as sleep } from 'node:timers/promises'; import { MockApplication } from 'egg-mock'; -import _mm from 'egg-mock'; +import { mm } from 'egg-mock'; -const mm = _mm.default; +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); describe('test/schedule.test.ts', () => { let app: MockApplication; @@ -14,19 +16,20 @@ describe('test/schedule.test.ts', () => { describe('schedule type worker', () => { it.only('should support interval and cron', async () => { app = mm.cluster({ baseDir: 'worker', workers: 2, cache: false }); - // app.debug(); + app.debug(); await app.ready(); await sleep(5000); const log = getLogContent('worker'); - // console.log(log); - assert(contains(log, 'interval') === 1); - assert(contains(log, 'cron') === 1); + console.log(log); + assert.equal(contains(log, 'interval'), 1); + assert.equal(contains(log, 'cron'), 1); const scheduleLog = getScheduleLogContent('worker'); - assert(contains(scheduleLog, 'cron.js executing by app') === 1); - assert(contains(scheduleLog, 'cron.js execute succeed') === 1); - assert(contains(scheduleLog, 'interval.js executing by app') === 1); - assert(contains(scheduleLog, 'interval.js execute succeed') === 1); + console.log(scheduleLog); + assert.equal(contains(scheduleLog, 'cron.js executing by app'), 1); + assert.equal(contains(scheduleLog, 'cron.js execute succeed'), 1); + assert.equal(contains(scheduleLog, 'interval.js executing by app'), 1); + assert.equal(contains(scheduleLog, 'interval.js execute succeed'), 1); }); it('should support ctxStorage', async () => { @@ -177,7 +180,7 @@ describe('test/schedule.test.ts', () => { await sleep(5000); const log = getLogContent('customTypeWithoutStart'); // console.log(log); - assert(contains(log, 'cluster_log') === 1); + assert.equal(contains(log, 'cluster_log'), 1); }); it('should handler error', async () => { @@ -185,8 +188,8 @@ describe('test/schedule.test.ts', () => { // app.debug(); await app.ready(); await sleep(1000); - // app.expect('code', 1); - // app.expect('stderr', /should provide clusterId/); + app.expect('code', 1); + app.expect('stderr', /should provide clusterId/); }); }); @@ -196,7 +199,7 @@ describe('test/schedule.test.ts', () => { // app.debug(); await app.ready(); await sleep(3000); - // app.expect('stderr', /schedule\.interval or schedule\.cron or schedule\.immediate must be present/); + app.expect('stderr', /schedule\.interval or schedule\.cron or schedule\.immediate must be present/); }); }); @@ -205,7 +208,7 @@ describe('test/schedule.test.ts', () => { app = mm.cluster({ baseDir: 'typeUndefined', workers: 2 }); await app.ready(); await sleep(3000); - // app.expect('stderr', /schedule type \[undefined\] is not defined/); + app.expect('stderr', /schedule type \[undefined\] is not defined/); }); }); @@ -215,7 +218,7 @@ describe('test/schedule.test.ts', () => { // app.debug(); await app.ready(); await sleep(1000); - // app.expect('stderr', /parse cron instruction\(invalid instruction\) error/); + app.expect('stderr', /parse cron instruction\(invalid instruction\) error/); }); }); @@ -257,7 +260,7 @@ describe('test/schedule.test.ts', () => { interval: 4000, }, }; - // app.agent.schedule.registerSchedule(schedule); + (app as any).agent.schedule.registerSchedule(schedule); app.scheduleWorker.registerSchedule(schedule as any); await app.runSchedule(key); @@ -282,10 +285,10 @@ describe('test/schedule.test.ts', () => { interval: 4000, }, }; - // app.agent.schedule.registerSchedule(schedule); + (app as any).agent.schedule.registerSchedule(schedule); app.scheduleWorker.registerSchedule(schedule as any); - // app.agent.schedule.unregisterSchedule(schedule.key); + (app as any).agent.schedule.unregisterSchedule(schedule.key); app.scheduleWorker.unregisterSchedule(schedule.key); let err: any; @@ -575,11 +578,6 @@ function getLogContent(name: string) { return fs.readFileSync(logPath, 'utf8'); } -// function getErrorLogContent(name) { -// const logPath = path.join(__dirname, 'fixtures', name, 'logs', name, 'common-error.log'); -// return fs.readFileSync(logPath, 'utf8'); -// } - function getAgentLogContent(name: string) { const logPath = path.join(__dirname, 'fixtures', name, 'logs', name, 'egg-agent.log'); return fs.readFileSync(logPath, 'utf8'); From 798efebab0f5baef40e2ebe216c81589474adc06 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Tue, 17 Dec 2024 18:12:14 +0800 Subject: [PATCH 04/10] f --- package.json | 2 + src/agent.ts | 11 +- src/app.ts | 24 ++-- src/lib/load_schedule.ts | 8 +- src/lib/schedule.ts | 12 +- src/lib/schedule_worker.ts | 2 +- src/lib/strategy/timer.ts | 6 +- .../app/schedule/cluster-all-clz.js | 2 +- .../app/schedule/cluster-clz.js | 2 +- .../app/schedule/interval.js | 10 ++ .../executeError-task-generator/package.json | 3 + .../executeError/app/schedule/interval.js | 4 +- .../generator/app/schedule/sub/cron.js | 12 -- test/fixtures/generator/app/service/user.js | 11 -- test/fixtures/generator/package.json | 3 - .../scheduleError/app/schedule/interval.js | 2 - .../scheduleError/app/schedule/sub/cron.js | 2 - .../app/schedule/interval.js | 2 +- .../app/schedule/sub/cron.js | 2 +- .../app/schedule/interval.js | 16 +++ .../app/schedule/sub/cron.js | 16 +++ .../subscription-generator/config/plugin.js | 1 + .../subscription-generator/package.json | 3 + .../subscription/app/schedule/interval.js | 4 +- .../subscription/app/schedule/sub/cron.js | 4 +- test/fixtures/subscription/config/plugin.js | 2 - test/fixtures/symlink/realFile.js | 8 +- test/fixtures/symlink/runDir/package.json | 3 +- test/fixtures/symlink/tsRealFile.ts | 6 +- test/schedule.test.ts | 130 +++++++++--------- 30 files changed, 176 insertions(+), 137 deletions(-) create mode 100644 test/fixtures/executeError-task-generator/app/schedule/interval.js create mode 100644 test/fixtures/executeError-task-generator/package.json delete mode 100644 test/fixtures/generator/app/schedule/sub/cron.js delete mode 100644 test/fixtures/generator/app/service/user.js delete mode 100644 test/fixtures/generator/package.json create mode 100644 test/fixtures/subscription-generator/app/schedule/interval.js create mode 100644 test/fixtures/subscription-generator/app/schedule/sub/cron.js create mode 100644 test/fixtures/subscription-generator/config/plugin.js create mode 100644 test/fixtures/subscription-generator/package.json diff --git a/package.json b/package.json index 939ab6d..33ed1be 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "cron" ], "dependencies": { + "@eggjs/utils": "^4.0.3", "cron-parser": "^4.9.0", "humanize-ms": "^2.0.0", "is-type-of": "^2.1.0", @@ -38,6 +39,7 @@ "@types/safe-timers": "^1.1.2", "egg": "beta", "egg-bin": "6", + "egg-logrotator": "^3.2.0", "egg-mock": "beta", "egg-tracer": "2", "eslint": "8", diff --git a/src/agent.ts b/src/agent.ts index a3e01e3..1461b00 100644 --- a/src/agent.ts +++ b/src/agent.ts @@ -25,11 +25,12 @@ export default class Boot implements ILifecycleBoot { // get job info from worker this.#agent.schedule.onJobFinish(info); }); + debug('didLoad'); + } - this.#agent.messenger.once('egg-ready', () => { - // start schedule after worker ready - this.#agent.schedule.start(); - debug('got egg-ready event, schedule start'); - }); + async serverDidReady(): Promise { + // start schedule after worker ready + this.#agent.schedule.start(); + debug('serverDidReady, schedule start'); } } diff --git a/src/app.ts b/src/app.ts index d2a3c77..5b20d3b 100644 --- a/src/app.ts +++ b/src/app.ts @@ -3,6 +3,7 @@ import path from 'node:path'; import type { Application, ILifecycleBoot, EggLogger, } from 'egg'; +import { importResolve } from '@eggjs/utils'; import { ScheduleItem, ScheduleJobInfo } from './lib/types.js'; const debug = debuglog('@eggjs/schedule/app'); @@ -16,7 +17,6 @@ export default class Boot implements ILifecycleBoot { } async didLoad(): Promise { - debug('didLoad'); const scheduleWorker = this.#app.scheduleWorker; await scheduleWorker.init(); @@ -24,7 +24,7 @@ export default class Boot implements ILifecycleBoot { for (const s in scheduleWorker.scheduleItems) { const schedule = scheduleWorker.scheduleItems[s]; if (!schedule.schedule.disable) { - this.#logger.info('[egg-schedule]: register schedule %s', schedule.key); + this.#logger.info('[@eggjs/schedule]: register schedule %s', schedule.key); } } @@ -66,9 +66,9 @@ export default class Boot implements ILifecycleBoot { return await schedule.task(ctx, ...info.args); }); success = true; - } catch (err) { + } catch (err: any) { success = false; - throw err; + e = err; } const rt = Date.now() - start; @@ -97,20 +97,24 @@ export default class Boot implements ILifecycleBoot { ...config.schedule.directory, ]; const runSchedule = async (schedulePath: string, ...args: any[]) => { + debug('[runSchedule] start schedulePath: %o, args: %o', schedulePath, args); + // resolve real path if (path.isAbsolute(schedulePath)) { - schedulePath = require.resolve(schedulePath); + schedulePath = importResolve(schedulePath); } else { for (const dir of directory) { + const trySchedulePath = path.join(dir, schedulePath); try { - schedulePath = require.resolve(path.join(dir, schedulePath)); + schedulePath = importResolve(trySchedulePath); break; - } catch (_) { - /* istanbul ignore next */ + } catch (err) { + debug('[runSchedule] importResolve %o error: %s', trySchedulePath, err); } } } + debug('[runSchedule] resolve schedulePath: %o', schedulePath); let schedule: ScheduleItem; try { schedule = scheduleWorker.scheduleItems[schedulePath]; @@ -118,7 +122,7 @@ export default class Boot implements ILifecycleBoot { throw new Error(`Cannot find schedule ${schedulePath}`); } } catch (err: any) { - err.message = `[egg-schedule] ${err.message}`; + err.message = `[@eggjs/schedule] ${err.message}`; throw err; } @@ -132,5 +136,7 @@ export default class Boot implements ILifecycleBoot { }); }; Reflect.set(this.#app, 'runSchedule', runSchedule); + + debug('didLoad'); } } diff --git a/src/lib/load_schedule.ts b/src/lib/load_schedule.ts index 160515d..dd6009f 100644 --- a/src/lib/load_schedule.ts +++ b/src/lib/load_schedule.ts @@ -1,7 +1,7 @@ import path from 'node:path'; import assert from 'node:assert'; import { stringify } from 'node:querystring'; -import { isClass, isFunction } from 'is-type-of'; +import { isClass, isFunction, isGeneratorFunction } from 'is-type-of'; import type { EggApplicationCore, EggContext } from 'egg'; import type { ScheduleConfig, ScheduleTask, ScheduleItem } from './types.js'; @@ -20,12 +20,16 @@ function getScheduleLoader(app: EggApplicationCore) { let task: ScheduleTask; if (isClass(schedule)) { + assert(!isGeneratorFunction(schedule.prototype.subscribe), + `schedule(${fullpath}): "schedule" generator function is not support, should use async function instead`); task = async (ctx: EggContext, ...args: any[]) => { const instance = new schedule(ctx); // s.subscribe = app.toAsyncFunction(s.subscribe); return instance.subscribe(...args); }; } else { + assert(!isGeneratorFunction(schedule.task), + `schedule(${fullpath}): "task" generator function is not support, should use async function instead`); task = schedule.task; // task = app.toAsyncFunction(schedule.task); } @@ -33,7 +37,7 @@ function getScheduleLoader(app: EggApplicationCore) { const env = app.config.env; const envList = schedule.schedule.env; if (Array.isArray(envList) && !envList.includes(env)) { - app.coreLogger.info(`[egg-schedule]: ignore schedule ${fullpath} due to \`schedule.env\` not match`); + app.coreLogger.info(`[@eggjs/schedule]: ignore schedule ${fullpath} due to \`schedule.env\` not match`); continue; } diff --git a/src/lib/schedule.ts b/src/lib/schedule.ts index 9d3df96..f2fa9f5 100644 --- a/src/lib/schedule.ts +++ b/src/lib/schedule.ts @@ -1,8 +1,11 @@ +import { debuglog } from 'node:util'; import type { Agent, EggLogger } from 'egg'; import { loadSchedule } from './load_schedule.js'; import type { ScheduleItem, ScheduleJobInfo } from './types.js'; import type { BaseStrategy } from './strategy/base.js'; +const debug = debuglog('@eggjs/schedule/lib/schedule'); + export class Schedule { closed = false; @@ -23,6 +26,7 @@ export class Schedule { */ use(type: string, clz: typeof BaseStrategy) { this.#strategyClassMap.set(type, clz); + debug('use type: %o', type); } /** @@ -38,7 +42,9 @@ export class Schedule { registerSchedule(scheduleItem: ScheduleItem) { const { key, schedule } = scheduleItem; const type = schedule.type; - if (schedule.disable) return; + if (schedule.disable) { + return; + } // find Strategy by type const Strategy = this.#strategyClassMap.get(type!); @@ -51,9 +57,11 @@ export class Schedule { // Initialize strategy and register const instance = new Strategy(schedule, this.#agent, key); this.#strategyInstanceMap.set(key, instance); + debug('registerSchedule type: %o, config: %o, key: %o', type, schedule, key); } unregisterSchedule(key: string) { + debug('unregisterSchedule key: %o', key); return this.#strategyInstanceMap.delete(key); } @@ -75,6 +83,7 @@ export class Schedule { * start schedule */ start() { + debug('start'); this.closed = false; for (const instance of this.#strategyInstanceMap.values()) { instance.start(); @@ -86,5 +95,6 @@ export class Schedule { for (const instance of this.#strategyInstanceMap.values()) { instance.close(); } + debug('close'); } } diff --git a/src/lib/schedule_worker.ts b/src/lib/schedule_worker.ts index 12bd8af..3f27680 100644 --- a/src/lib/schedule_worker.ts +++ b/src/lib/schedule_worker.ts @@ -4,7 +4,7 @@ import type { ScheduleItem } from './types.js'; export class ScheduleWorker { #app: Application; - scheduleItems: Record; + scheduleItems: Record = {}; constructor(app: Application) { this.#app = app; diff --git a/src/lib/strategy/timer.ts b/src/lib/strategy/timer.ts index 34f400c..ee20314 100644 --- a/src/lib/strategy/timer.ts +++ b/src/lib/strategy/timer.ts @@ -15,7 +15,7 @@ export abstract class TimerStrategy extends BaseStrategy { const { interval, cron, cronOptions, immediate } = this.scheduleConfig; assert(interval || cron || immediate, - `[egg-schedule] ${this.key} \`schedule.interval\` or \`schedule.cron]\` or \`schedule.immediate\` must be present`); + `[@eggjs/schedule] ${this.key} \`schedule.interval\` or \`schedule.cron\` or \`schedule.immediate\` must be present`); // init cron parser if (cron) { @@ -23,14 +23,14 @@ export abstract class TimerStrategy extends BaseStrategy { this.cronInstance = parseExpression(cron, cronOptions); } catch (err: any) { throw new TypeError( - `[egg-schedule] ${this.key} parse cron instruction(${cron}) error: ${err.message}`, + `[@eggjs/schedule] ${this.key} parse cron instruction(${cron}) error: ${err.message}`, { cause: err }); } } } protected handler() { - throw new TypeError(`[egg-schedule] ${this.key} strategy should override \`handler()\` method`); + throw new TypeError(`[@eggjs/schedule] ${this.key} strategy should override \`handler()\` method`); } diff --git a/test/fixtures/customTypeParams/app/schedule/cluster-all-clz.js b/test/fixtures/customTypeParams/app/schedule/cluster-all-clz.js index e1a4ee1..86a41fe 100644 --- a/test/fixtures/customTypeParams/app/schedule/cluster-all-clz.js +++ b/test/fixtures/customTypeParams/app/schedule/cluster-all-clz.js @@ -10,7 +10,7 @@ class Interval extends Subscription { }; } - * subscribe(data) { + async subscribe(data) { this.ctx.logger.info('cluster_all_log_clz', data); } } diff --git a/test/fixtures/customTypeParams/app/schedule/cluster-clz.js b/test/fixtures/customTypeParams/app/schedule/cluster-clz.js index 78a61a4..2d27b41 100644 --- a/test/fixtures/customTypeParams/app/schedule/cluster-clz.js +++ b/test/fixtures/customTypeParams/app/schedule/cluster-clz.js @@ -10,7 +10,7 @@ class Interval extends Subscription { }; } - * subscribe(data) { + async subscribe(data) { this.ctx.logger.info('cluster_log_clz', data); } } diff --git a/test/fixtures/executeError-task-generator/app/schedule/interval.js b/test/fixtures/executeError-task-generator/app/schedule/interval.js new file mode 100644 index 0000000..e45ecf5 --- /dev/null +++ b/test/fixtures/executeError-task-generator/app/schedule/interval.js @@ -0,0 +1,10 @@ +'use strict'; + +exports.schedule = { + type: 'worker', + interval: 2000, +}; + +exports.task = function* () { + throw new Error('interval error'); +}; diff --git a/test/fixtures/executeError-task-generator/package.json b/test/fixtures/executeError-task-generator/package.json new file mode 100644 index 0000000..55cef39 --- /dev/null +++ b/test/fixtures/executeError-task-generator/package.json @@ -0,0 +1,3 @@ +{ + "name": "executeError" +} diff --git a/test/fixtures/executeError/app/schedule/interval.js b/test/fixtures/executeError/app/schedule/interval.js index e45ecf5..1fd89db 100644 --- a/test/fixtures/executeError/app/schedule/interval.js +++ b/test/fixtures/executeError/app/schedule/interval.js @@ -1,10 +1,8 @@ -'use strict'; - exports.schedule = { type: 'worker', interval: 2000, }; -exports.task = function* () { +exports.task = async function() { throw new Error('interval error'); }; diff --git a/test/fixtures/generator/app/schedule/sub/cron.js b/test/fixtures/generator/app/schedule/sub/cron.js deleted file mode 100644 index a813701..0000000 --- a/test/fixtures/generator/app/schedule/sub/cron.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -exports.schedule = { - type: 'worker', - cron: '*/5 * * * * *', -}; - -exports.task = function* (ctx) { - ctx.logger.info(`method: ${ctx.method}, path: ${ctx.path}, query: ${JSON.stringify(ctx.query)}`); - const msg = yield ctx.service.user.hello('busi'); - ctx.logger.info(msg); -}; diff --git a/test/fixtures/generator/app/service/user.js b/test/fixtures/generator/app/service/user.js deleted file mode 100644 index a516816..0000000 --- a/test/fixtures/generator/app/service/user.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -const Service = require('egg').Service; - -class UserService extends Service { - * hello(name) { - return `hello ${name}`; - } -} - -module.exports = UserService; diff --git a/test/fixtures/generator/package.json b/test/fixtures/generator/package.json deleted file mode 100644 index 623456b..0000000 --- a/test/fixtures/generator/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "generator" -} diff --git a/test/fixtures/scheduleError/app/schedule/interval.js b/test/fixtures/scheduleError/app/schedule/interval.js index 49971e4..dbff82e 100644 --- a/test/fixtures/scheduleError/app/schedule/interval.js +++ b/test/fixtures/scheduleError/app/schedule/interval.js @@ -1,5 +1,3 @@ -'use strict'; - exports.schedule = { type: 'worker', }; diff --git a/test/fixtures/scheduleError/app/schedule/sub/cron.js b/test/fixtures/scheduleError/app/schedule/sub/cron.js index 442e7e5..de3d686 100644 --- a/test/fixtures/scheduleError/app/schedule/sub/cron.js +++ b/test/fixtures/scheduleError/app/schedule/sub/cron.js @@ -1,5 +1,3 @@ -'use strict'; - exports.schedule = { type: 'worker', cron: '*/5 * * * * *', diff --git a/test/fixtures/subscription-enableFastContextLogger/app/schedule/interval.js b/test/fixtures/subscription-enableFastContextLogger/app/schedule/interval.js index 665593a..52c0641 100644 --- a/test/fixtures/subscription-enableFastContextLogger/app/schedule/interval.js +++ b/test/fixtures/subscription-enableFastContextLogger/app/schedule/interval.js @@ -8,7 +8,7 @@ class Interval extends Subscription { }; } - * subscribe() { + async subscribe() { this.app.logger.info('interval'); } } diff --git a/test/fixtures/subscription-enableFastContextLogger/app/schedule/sub/cron.js b/test/fixtures/subscription-enableFastContextLogger/app/schedule/sub/cron.js index 4bc9e80..cc3c5c2 100644 --- a/test/fixtures/subscription-enableFastContextLogger/app/schedule/sub/cron.js +++ b/test/fixtures/subscription-enableFastContextLogger/app/schedule/sub/cron.js @@ -8,7 +8,7 @@ class Interval extends Subscription { }; } - * subscribe() { + async subscribe() { this.app.logger.info('cron'); } } diff --git a/test/fixtures/subscription-generator/app/schedule/interval.js b/test/fixtures/subscription-generator/app/schedule/interval.js new file mode 100644 index 0000000..7bfec26 --- /dev/null +++ b/test/fixtures/subscription-generator/app/schedule/interval.js @@ -0,0 +1,16 @@ +const Subscription = require('egg').Subscription; + +class Interval extends Subscription { + static get schedule() { + return { + type: 'worker', + interval: 4000, + }; + } + + * subscribe() { + this.ctx.logger.info('interval'); + } +} + +module.exports = Interval; diff --git a/test/fixtures/subscription-generator/app/schedule/sub/cron.js b/test/fixtures/subscription-generator/app/schedule/sub/cron.js new file mode 100644 index 0000000..b8fa02b --- /dev/null +++ b/test/fixtures/subscription-generator/app/schedule/sub/cron.js @@ -0,0 +1,16 @@ +const Subscription = require('egg').Subscription; + +class Interval extends Subscription { + static get schedule() { + return { + type: 'worker', + cron: '*/5 * * * * *', + }; + } + + async subscribe() { + this.ctx.logger.info('cron'); + } +} + +module.exports = Interval; diff --git a/test/fixtures/subscription-generator/config/plugin.js b/test/fixtures/subscription-generator/config/plugin.js new file mode 100644 index 0000000..337392c --- /dev/null +++ b/test/fixtures/subscription-generator/config/plugin.js @@ -0,0 +1 @@ +exports.logrotator = true; diff --git a/test/fixtures/subscription-generator/package.json b/test/fixtures/subscription-generator/package.json new file mode 100644 index 0000000..c502936 --- /dev/null +++ b/test/fixtures/subscription-generator/package.json @@ -0,0 +1,3 @@ +{ + "name": "subscription" +} diff --git a/test/fixtures/subscription/app/schedule/interval.js b/test/fixtures/subscription/app/schedule/interval.js index ff9758d..a8423e3 100644 --- a/test/fixtures/subscription/app/schedule/interval.js +++ b/test/fixtures/subscription/app/schedule/interval.js @@ -1,5 +1,3 @@ -'use strict'; - const Subscription = require('egg').Subscription; class Interval extends Subscription { @@ -10,7 +8,7 @@ class Interval extends Subscription { }; } - * subscribe() { + async subscribe() { this.ctx.logger.info('interval'); } } diff --git a/test/fixtures/subscription/app/schedule/sub/cron.js b/test/fixtures/subscription/app/schedule/sub/cron.js index 587492d..b8fa02b 100644 --- a/test/fixtures/subscription/app/schedule/sub/cron.js +++ b/test/fixtures/subscription/app/schedule/sub/cron.js @@ -1,5 +1,3 @@ -'use strict'; - const Subscription = require('egg').Subscription; class Interval extends Subscription { @@ -10,7 +8,7 @@ class Interval extends Subscription { }; } - * subscribe() { + async subscribe() { this.ctx.logger.info('cron'); } } diff --git a/test/fixtures/subscription/config/plugin.js b/test/fixtures/subscription/config/plugin.js index e54d182..337392c 100644 --- a/test/fixtures/subscription/config/plugin.js +++ b/test/fixtures/subscription/config/plugin.js @@ -1,3 +1 @@ -'use strict'; - exports.logrotator = true; diff --git a/test/fixtures/symlink/realFile.js b/test/fixtures/symlink/realFile.js index 2157971..b48137c 100644 --- a/test/fixtures/symlink/realFile.js +++ b/test/fixtures/symlink/realFile.js @@ -1,10 +1,8 @@ -'use strict'; - -exports.schedule = { +export const schedule = { type: 'worker', interval: '4s', }; -exports.task = async function (ctx) { +export async function task(ctx) { ctx.logger.info('interval'); -}; +} diff --git a/test/fixtures/symlink/runDir/package.json b/test/fixtures/symlink/runDir/package.json index 030f5f6..55e9a32 100644 --- a/test/fixtures/symlink/runDir/package.json +++ b/test/fixtures/symlink/runDir/package.json @@ -1,3 +1,4 @@ { - "name": "symlink" + "name": "symlink", + "type": "module" } diff --git a/test/fixtures/symlink/tsRealFile.ts b/test/fixtures/symlink/tsRealFile.ts index 29efc16..b48137c 100644 --- a/test/fixtures/symlink/tsRealFile.ts +++ b/test/fixtures/symlink/tsRealFile.ts @@ -1,8 +1,8 @@ -exports.schedule = { +export const schedule = { type: 'worker', interval: '4s', }; -exports.task = async function (ctx) { +export async function task(ctx) { ctx.logger.info('interval'); -}; +} diff --git a/test/schedule.test.ts b/test/schedule.test.ts index d343a4e..fb1b1e0 100644 --- a/test/schedule.test.ts +++ b/test/schedule.test.ts @@ -3,8 +3,8 @@ import path from 'node:path'; import fs from 'node:fs'; import { fileURLToPath } from 'node:url'; import { setTimeout as sleep } from 'node:timers/promises'; -import { MockApplication } from 'egg-mock'; -import { mm } from 'egg-mock'; +import { importResolve } from '@eggjs/utils'; +import { mm, MockApplication } from 'egg-mock'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -14,7 +14,7 @@ describe('test/schedule.test.ts', () => { afterEach(() => app.close()); describe('schedule type worker', () => { - it.only('should support interval and cron', async () => { + it('should support interval and cron', async () => { app = mm.cluster({ baseDir: 'worker', workers: 2, cache: false }); app.debug(); await app.ready(); @@ -90,20 +90,6 @@ describe('test/schedule.test.ts', () => { assert(/hello busi/.test(log)); }); - it('should support generator', async () => { - app = mm.cluster({ baseDir: 'generator', workers: 2 }); - await app.ready(); - await sleep(5000); - const log = getLogContent('generator'); - // console.log(log); - assert(/method: SCHEDULE/.test(log)); - assert(/path: \/__schedule/.test(log)); - assert(/(.*?)sub(\/|\\)cron\.js/.test(log)); - assert(/"type":"worker"/.test(log)); - assert(/"cron":"\*\/5 \* \* \* \* \*"/.test(log)); - assert(/hello busi/.test(log)); - }); - it('should support immediate', async () => { app = mm.cluster({ baseDir: 'immediate', workers: 2 }); await app.ready(); @@ -198,8 +184,8 @@ describe('test/schedule.test.ts', () => { app = mm.cluster({ baseDir: 'scheduleError', workers: 2 }); // app.debug(); await app.ready(); - await sleep(3000); - app.expect('stderr', /schedule\.interval or schedule\.cron or schedule\.immediate must be present/); + await sleep(5000); + app.expect('stderr', /`schedule\.interval` or `schedule\.cron` or `schedule\.immediate` must be present/); }); }); @@ -207,7 +193,7 @@ describe('test/schedule.test.ts', () => { it('should thrown', async () => { app = mm.cluster({ baseDir: 'typeUndefined', workers: 2 }); await app.ready(); - await sleep(3000); + await sleep(5000); app.expect('stderr', /schedule type \[undefined\] is not defined/); }); }); @@ -235,11 +221,20 @@ describe('test/schedule.test.ts', () => { describe('schedule execute error', () => { it('should thrown', async () => { app = mm.cluster({ baseDir: 'executeError', workers: 1 }); - // app.debug(); + app.debug(); await app.ready(); await sleep(5000); const scheduleLog = getScheduleLogContent('executeError'); - assert(contains(scheduleLog, 'interval.js execute failed') === 2); + assert.equal(contains(scheduleLog, 'interval.js execute failed'), 2); + }); + }); + + describe('schedule execute task is generator function', () => { + it('should thrown', async () => { + app = mm.cluster({ baseDir: 'executeError-task-generator', workers: 1 }); + app.debug(); + await app.ready(); + app.expect('stderr', /"task" generator function is not support, should use async function instead/); }); }); @@ -339,7 +334,7 @@ describe('test/schedule.test.ts', () => { it('should run schedule by absolute package path success', async () => { app = mm.app({ baseDir: 'worker', cache: false }); await app.ready(); - await app.runSchedule(require.resolve('../node_modules/egg-logrotator/app/schedule/rotate_by_file.js')); + await app.runSchedule(importResolve('egg-logrotator/app/schedule/rotate_by_file.js')); }); it('should run schedule by relative path success at customDirectory', async () => { @@ -371,47 +366,51 @@ describe('test/schedule.test.ts', () => { }, }); await app.runSchedule('sub/foobar', 'use app.logger.info should work'); - await sleep(1000); + await sleep(5000); const log = getLogContent('worker2'); // console.log(log); - assert.match(log, / \[-\/127.0.0.1\/mock-trace-123\/\d+ms GET \/] foobar use app.logger.info should work/); + assert.match(log, / \[-\/127.0.0.1\/mock-trace-123\/[\d\.]+ms GET \/] foobar use app.logger.info should work/); }); it('should run schedule with symlink js file success', async () => { const realPath = path.join(__dirname, 'fixtures/symlink/realFile.js'); const targetPath = path.join(__dirname, 'fixtures/symlink/runDir/app/schedule/realFile.js'); - fs.symlinkSync(realPath, targetPath); - - app = mm.app({ baseDir: 'symlink/runDir', cache: false }); - await app.ready(); try { - await app.runSchedule('realFile'); - } catch (err) { - assert(false, 'should not throw Cannot find schedule error'); + fs.unlinkSync(targetPath); + } catch { + // ignore + } + try { + fs.symlinkSync(realPath, targetPath); + } catch { + // ignore } + app = mm.app({ baseDir: 'symlink/runDir', cache: false }); + await app.ready(); + await app.runSchedule('realFile'); fs.unlinkSync(targetPath); }); - it('should run schedule with symlink ts file success', async () => { - mm(process.env, 'EGG_TYPESCRIPT', 'true'); - require.extensions['.ts'] = require.extensions['.js']; + // it.skip('should run schedule with symlink ts file success', async () => { + // mm(process.env, 'EGG_TYPESCRIPT', 'true'); + // require.extensions['.ts'] = require.extensions['.js']; - const realPath = path.join(__dirname, 'fixtures/symlink/tsRealFile.ts'); - const targetPath = path.join(__dirname, 'fixtures/symlink/runDir/app/schedule/tsRealFile.ts'); - fs.symlinkSync(realPath, targetPath); + // const realPath = path.join(__dirname, 'fixtures/symlink/tsRealFile.ts'); + // const targetPath = path.join(__dirname, 'fixtures/symlink/runDir/app/schedule/tsRealFile.ts'); + // fs.symlinkSync(realPath, targetPath); - app = mm.app({ baseDir: 'symlink/runDir', cache: false }); - await app.ready(); - try { - await app.runSchedule('tsRealFile'); - } catch (err) { - assert(false, 'should not throw Cannot find schedule error'); - } + // app = mm.app({ baseDir: 'symlink/runDir', cache: false }); + // await app.ready(); + // try { + // await app.runSchedule('tsRealFile'); + // } catch (err) { + // assert(false, 'should not throw Cannot find schedule error'); + // } - delete require.extensions['.ts']; - fs.unlinkSync(targetPath); - }); + // delete require.extensions['.ts']; + // fs.unlinkSync(targetPath); + // }); }); describe('stop schedule', () => { @@ -449,12 +448,11 @@ describe('test/schedule.test.ts', () => { }); }); - describe('export schedules', () => { + describe('export app.schedules', () => { it('should export app.schedules', async () => { app = mm.app({ baseDir: 'worker', cache: false }); await app.ready(); - assert('schedules' in app); - assert(Reflect.get(app, 'schedules')); + assert(app.schedules); }); }); @@ -479,26 +477,34 @@ describe('test/schedule.test.ts', () => { describe('Subscription', () => { it('should support interval and cron', async () => { app = mm.cluster({ baseDir: 'subscription', workers: 2, cache: false }); - // app.debug(); + app.debug(); await app.ready(); await sleep(5000); const log = getLogContent('subscription'); // console.log(log); - assert(contains(log, 'interval') === 1); - assert(contains(log, 'cron') === 1); + assert.equal(contains(log, 'interval'), 1); + assert.equal(contains(log, 'cron'), 1); + }); + + it('should throw error on generator function', async () => { + app = mm.cluster({ baseDir: 'subscription-generator', workers: 2, cache: false }); + app.debug(); + await app.ready(); + await sleep(3000); + app.expect('stderr', /"schedule" generator function is not support, should use async function instead/); }); it('should support interval and cron when config.logger.enableFastContextLogger = true', async () => { app = mm.cluster({ baseDir: 'subscription-enableFastContextLogger', workers: 2, cache: false }); - // app.debug(); + app.debug(); await app.ready(); await sleep(5000); const log = getLogContent('subscription-enableFastContextLogger'); - // console.log(log); - assert(contains(log, 'interval') === 1); - assert(contains(log, 'cron') === 1); + console.log(log); + assert.equal(contains(log, 'interval'), 1); + assert.equal(contains(log, 'cron'), 1); // 2022-12-11 16:44:55,009 INFO 22958 [-/127.0.0.1/15d62420-7930-11ed-86ce-31ec9c2e0d18/3ms SCHEDULE /__schedule - assert.match(log, / INFO \w+ \[-\/127\.0\.0\.1\/\w+\-\w+\-\w+\-\w+\-\w+\/\d+ms SCHEDULE \/__schedule/); + assert.match(log, / INFO \w+ \[-\/127\.0\.0\.1\/\w+\-\w+\-\w+\-\w+\-\w+\/[\d\.]+ms SCHEDULE \/__schedule/); }); }); @@ -561,9 +567,9 @@ describe('test/schedule.test.ts', () => { await sleep(2000); const scheduleLog = getScheduleLogContent('detect-error'); - assert(contains(scheduleLog, 'suc.js execute succeed') === 1); - assert(contains(scheduleLog, /fail.js execute failed, used \d+ms. Error: fail/) === 1); - assert(contains(scheduleLog, /error.js execute failed, used \d+ms. Error: some err/) === 1); + assert.equal(contains(scheduleLog, 'suc.js execute succeed'), 1); + assert.equal(contains(scheduleLog, /fail.js execute failed, used [\d\.]+ms. fail/), 1); + assert.equal(contains(scheduleLog, /error.js execute failed, used [\d\.]+ms. Error: some err/), 1); }); }); }); From e229d727421ee16a809e6b68bbe54fd5b9eac0a0 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Tue, 17 Dec 2024 18:23:32 +0800 Subject: [PATCH 05/10] f --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 33ed1be..2012bb9 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "@types/mocha": "10", "@types/node": "22", "@types/safe-timers": "^1.1.2", - "egg": "beta", + "egg": "^4.0.0-beta.3", "egg-bin": "6", "egg-logrotator": "^3.2.0", "egg-mock": "beta", From 9c1620ea1d6088adb50c54da3e197807a70f6409 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Tue, 17 Dec 2024 18:29:02 +0800 Subject: [PATCH 06/10] f --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 2012bb9..e495d03 100644 --- a/package.json +++ b/package.json @@ -37,8 +37,8 @@ "@types/mocha": "10", "@types/node": "22", "@types/safe-timers": "^1.1.2", - "egg": "^4.0.0-beta.3", - "egg-bin": "6", + "egg": "beta", + "egg-bin": "beta", "egg-logrotator": "^3.2.0", "egg-mock": "beta", "egg-tracer": "2", From 6c8d855ab591dc674d909ec654cdd8c8f3662034 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Tue, 17 Dec 2024 18:32:31 +0800 Subject: [PATCH 07/10] f --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index e495d03..7953244 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,9 @@ { "name": "@eggjs/schedule", "version": "4.0.1", + "publishConfig": { + "access": "public" + }, "engines": { "node": ">=18.19.0" }, From 2c558cbe1a0f1c0f4ef5faa089993cc608636637 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Tue, 17 Dec 2024 18:52:08 +0800 Subject: [PATCH 08/10] f --- README.md | 4 +-- package.json | 4 +-- src/agent.ts | 4 +-- src/app.ts | 6 ++--- src/index.ts | 1 + src/lib/load_schedule.ts | 10 ++++---- src/lib/schedule.ts | 6 ++--- src/lib/schedule_worker.ts | 6 ++--- src/lib/strategy/base.ts | 16 ++++++------ src/lib/strategy/timer.ts | 4 +-- src/lib/types.ts | 28 +++++++++++++-------- test/fixtures/demo/config/config.default.ts | 11 ++++++++ test/fixtures/demo/package.json | 0 13 files changed, 60 insertions(+), 40 deletions(-) create mode 100644 src/index.ts create mode 100644 test/fixtures/demo/config/config.default.ts create mode 100644 test/fixtures/demo/package.json diff --git a/README.md b/README.md index 37ee3d2..4e0a79e 100644 --- a/README.md +++ b/README.md @@ -258,7 +258,7 @@ See `${appInfo.root}/logs/{app_name}/egg-schedule.log` which provided by [config // config/config.default.ts import { EggAppConfig } from 'egg'; -export default const config = { +export default { customLogger: { scheduleLogger: { // consoleLevel: 'NONE', @@ -276,7 +276,7 @@ If you want to add additional schedule directories, you can use this config. // config/config.default.ts import { EggAppConfig } from 'egg'; -export default const config = { +export default { schedule: { directory: [ 'path/to/otherSchedule', diff --git a/package.json b/package.json index 7953244..7f02be3 100644 --- a/package.json +++ b/package.json @@ -55,8 +55,8 @@ "lint": "eslint --cache src test --ext .ts", "pretest": "npm run lint -- --fix && npm run prepublishOnly", "test": "egg-bin test", - "preci": "npm run lint && npm run prepublishOnly", - "ci": "egg-bin cov && attw --pack", + "preci": "npm run lint && npm run prepublishOnly && attw --pack", + "ci": "egg-bin cov", "prepublishOnly": "tshy && tshy-after" }, "author": "dead_horse", diff --git a/src/agent.ts b/src/agent.ts index 1461b00..2adb814 100644 --- a/src/agent.ts +++ b/src/agent.ts @@ -2,7 +2,7 @@ import { debuglog } from 'node:util'; import type { Agent, ILifecycleBoot } from 'egg'; import { WorkerStrategy } from './lib/strategy/worker.js'; import { AllStrategy } from './lib/strategy/all.js'; -import { ScheduleJobInfo } from './lib/types.js'; +import { EggScheduleJobInfo } from './lib/types.js'; const debug = debuglog('@eggjs/schedule/agent'); @@ -21,7 +21,7 @@ export default class Boot implements ILifecycleBoot { await this.#agent.schedule.init(); // dispatch job finish event to strategy - this.#agent.messenger.on('egg-schedule', (info: ScheduleJobInfo) => { + this.#agent.messenger.on('egg-schedule', (info: EggScheduleJobInfo) => { // get job info from worker this.#agent.schedule.onJobFinish(info); }); diff --git a/src/app.ts b/src/app.ts index 5b20d3b..463347e 100644 --- a/src/app.ts +++ b/src/app.ts @@ -4,7 +4,7 @@ import type { Application, ILifecycleBoot, EggLogger, } from 'egg'; import { importResolve } from '@eggjs/utils'; -import { ScheduleItem, ScheduleJobInfo } from './lib/types.js'; +import { EggScheduleItem, EggScheduleJobInfo } from './lib/types.js'; const debug = debuglog('@eggjs/schedule/app'); @@ -87,7 +87,7 @@ export default class Boot implements ILifecycleBoot { workerId: process.pid, rt, message: e?.message, - } as ScheduleJobInfo); + } as EggScheduleJobInfo); }); // for test purpose @@ -115,7 +115,7 @@ export default class Boot implements ILifecycleBoot { } debug('[runSchedule] resolve schedulePath: %o', schedulePath); - let schedule: ScheduleItem; + let schedule: EggScheduleItem; try { schedule = scheduleWorker.scheduleItems[schedulePath]; if (!schedule) { diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..f6ebe91 --- /dev/null +++ b/src/index.ts @@ -0,0 +1 @@ +export * from './lib/types.js'; diff --git a/src/lib/load_schedule.ts b/src/lib/load_schedule.ts index dd6009f..c9410a0 100644 --- a/src/lib/load_schedule.ts +++ b/src/lib/load_schedule.ts @@ -3,22 +3,22 @@ import assert from 'node:assert'; import { stringify } from 'node:querystring'; import { isClass, isFunction, isGeneratorFunction } from 'is-type-of'; import type { EggApplicationCore, EggContext } from 'egg'; -import type { ScheduleConfig, ScheduleTask, ScheduleItem } from './types.js'; +import type { EggScheduleConfig, EggScheduleTask, EggScheduleItem } from './types.js'; function getScheduleLoader(app: EggApplicationCore) { return class ScheduleLoader extends app.loader.FileLoader { async load() { - const target = this.options.target as Record; + const target = this.options.target as Record; const items = await this.parse(); for (const item of items) { - const schedule = item.exports as { schedule: ScheduleConfig, task: ScheduleTask }; + const schedule = item.exports as { schedule: EggScheduleConfig, task: EggScheduleTask }; const fullpath = item.fullpath; const scheduleConfig = schedule.schedule; assert(scheduleConfig, `schedule(${fullpath}): must have "schedule" and "task" properties`); assert(isClass(schedule) || isFunction(schedule.task), `schedule(${fullpath}: \`schedule.task\` should be function or \`schedule\` should be class`); - let task: ScheduleTask; + let task: EggScheduleTask; if (isClass(schedule)) { assert(!isGeneratorFunction(schedule.prototype.subscribe), `schedule(${fullpath}): "schedule" generator function is not support, should use async function instead`); @@ -62,7 +62,7 @@ export async function loadSchedule(app: EggApplicationCore) { ]; const Loader = getScheduleLoader(app); - const schedules = {} as Record; + const schedules = {} as Record; await new Loader({ directory: dirs, target: schedules, diff --git a/src/lib/schedule.ts b/src/lib/schedule.ts index f2fa9f5..3c28d18 100644 --- a/src/lib/schedule.ts +++ b/src/lib/schedule.ts @@ -1,7 +1,7 @@ import { debuglog } from 'node:util'; import type { Agent, EggLogger } from 'egg'; import { loadSchedule } from './load_schedule.js'; -import type { ScheduleItem, ScheduleJobInfo } from './types.js'; +import type { EggScheduleItem, EggScheduleJobInfo } from './types.js'; import type { BaseStrategy } from './strategy/base.js'; const debug = debuglog('@eggjs/schedule/lib/schedule'); @@ -39,7 +39,7 @@ export class Schedule { } } - registerSchedule(scheduleItem: ScheduleItem) { + registerSchedule(scheduleItem: EggScheduleItem) { const { key, schedule } = scheduleItem; const type = schedule.type; if (schedule.disable) { @@ -70,7 +70,7 @@ export class Schedule { * * @param {Object} info - { id, key, success, message, workerId } */ - onJobFinish(info: ScheduleJobInfo) { + onJobFinish(info: EggScheduleJobInfo) { this.#logger.debug(`[Job#${info.id}] ${info.key} finish event received by agent from worker#${info.workerId}`); const instance = this.#strategyInstanceMap.get(info.key); /* istanbul ignore else */ diff --git a/src/lib/schedule_worker.ts b/src/lib/schedule_worker.ts index 3f27680..415ea20 100644 --- a/src/lib/schedule_worker.ts +++ b/src/lib/schedule_worker.ts @@ -1,10 +1,10 @@ import type { Application } from 'egg'; import { loadSchedule } from './load_schedule.js'; -import type { ScheduleItem } from './types.js'; +import type { EggScheduleItem } from './types.js'; export class ScheduleWorker { #app: Application; - scheduleItems: Record = {}; + scheduleItems: Record = {}; constructor(app: Application) { this.#app = app; @@ -14,7 +14,7 @@ export class ScheduleWorker { this.scheduleItems = await loadSchedule(this.#app); } - registerSchedule(scheduleItem: ScheduleItem) { + registerSchedule(scheduleItem: EggScheduleItem) { this.scheduleItems[scheduleItem.key] = scheduleItem; } diff --git a/src/lib/strategy/base.ts b/src/lib/strategy/base.ts index 5b48b23..8b83834 100644 --- a/src/lib/strategy/base.ts +++ b/src/lib/strategy/base.ts @@ -1,15 +1,15 @@ import type { Agent, EggLogger } from 'egg'; -import type { ScheduleConfig, ScheduleJobInfo } from '../types.js'; +import type { EggScheduleConfig, EggScheduleJobInfo } from '../types.js'; export class BaseStrategy { protected agent: Agent; - protected scheduleConfig: ScheduleConfig; + protected scheduleConfig: EggScheduleConfig; protected key: string; protected logger: EggLogger; protected closed = false; count = 0; - constructor(scheduleConfig: ScheduleConfig, agent: Agent, key: string) { + constructor(scheduleConfig: EggScheduleConfig, agent: Agent, key: string) { this.agent = agent; this.key = key; this.scheduleConfig = scheduleConfig; @@ -17,7 +17,7 @@ export class BaseStrategy { } /** keep compatibility */ - get schedule(): ScheduleConfig { + get schedule(): EggScheduleConfig { return this.scheduleConfig; } @@ -30,10 +30,10 @@ export class BaseStrategy { } // eslint-disable-next-line @typescript-eslint/no-unused-vars - onJobStart(_info: ScheduleJobInfo) {} + onJobStart(_info: EggScheduleJobInfo) {} // eslint-disable-next-line @typescript-eslint/no-unused-vars - onJobFinish(_info: ScheduleJobInfo) {} + onJobFinish(_info: EggScheduleJobInfo) {} /** * trigger one worker @@ -53,7 +53,7 @@ export class BaseStrategy { key: this.key, id: this.getSeqId(), args, - } as ScheduleJobInfo; + } as EggScheduleJobInfo; this.logger.info(`[Job#${info.id}] ${info.key} triggered, send random by agent`); this.agent.messenger.sendRandom('egg-schedule', info); @@ -78,7 +78,7 @@ export class BaseStrategy { key: this.key, id: this.getSeqId(), args, - } as ScheduleJobInfo; + } as EggScheduleJobInfo; this.logger.info(`[Job#${info.id}] ${info.key} triggered, send all by agent`); // send to all workers this.agent.messenger.send('egg-schedule', info); diff --git a/src/lib/strategy/timer.ts b/src/lib/strategy/timer.ts index ee20314..939ea72 100644 --- a/src/lib/strategy/timer.ts +++ b/src/lib/strategy/timer.ts @@ -4,13 +4,13 @@ import { ms } from 'humanize-ms'; import safeTimers from 'safe-timers'; import { logDate } from 'utility'; import type { Agent } from 'egg'; -import type { ScheduleConfig } from '../types.js'; +import type { EggScheduleConfig } from '../types.js'; import { BaseStrategy } from './base.js'; export abstract class TimerStrategy extends BaseStrategy { protected cronInstance?: CronExpression; - constructor(scheduleConfig: ScheduleConfig, agent: Agent, key: string) { + constructor(scheduleConfig: EggScheduleConfig, agent: Agent, key: string) { super(scheduleConfig, agent, key); const { interval, cron, cronOptions, immediate } = this.scheduleConfig; diff --git a/src/lib/types.ts b/src/lib/types.ts index 4321e26..36d8e38 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -6,7 +6,7 @@ import type { ScheduleWorker } from './schedule_worker.js'; * Schedule Config * @see https://www.eggjs.org/zh-CN/basics/schedule */ -export interface ScheduleConfig { +export interface EggScheduleConfig { type?: 'worker' | 'all'; interval?: string | number; cron?: string; @@ -16,16 +16,16 @@ export interface ScheduleConfig { env?: string[]; } -export type ScheduleTask = (ctx: any, ...args: any[]) => Promise; +export type EggScheduleTask = (ctx: any, ...args: any[]) => Promise; -export interface ScheduleItem { - schedule: ScheduleConfig; +export interface EggScheduleItem { + schedule: EggScheduleConfig; scheduleQueryString: string; - task: ScheduleTask; + task: EggScheduleTask; key: string; } -export interface ScheduleJobInfo { +export interface EggScheduleJobInfo { id: string; key: string; workerId: number; @@ -36,15 +36,23 @@ export interface ScheduleJobInfo { } declare module 'egg' { - export interface ScheduleAgent { + export interface EggScheduleAgent { schedule: Schedule; } - export interface Agent extends ScheduleAgent {} + export interface Agent extends EggScheduleAgent {} - export interface ScheduleApplication { + export interface EggScheduleApplication { scheduleWorker: ScheduleWorker; /** runSchedule in unittest */ runSchedule: (schedulePath: string, ...args: any[]) => Promise; } - export interface Application extends ScheduleApplication {} + export interface Application extends EggScheduleApplication {} + + export interface EggScheduleAppConfig { + schedule: { + directory: string[]; + }; + } + + export interface EggAppConfig extends EggScheduleAppConfig {} } diff --git a/test/fixtures/demo/config/config.default.ts b/test/fixtures/demo/config/config.default.ts new file mode 100644 index 0000000..3508e28 --- /dev/null +++ b/test/fixtures/demo/config/config.default.ts @@ -0,0 +1,11 @@ +import '../../../../src/index.js'; + +import { EggAppConfig } from 'egg'; + +export default { + schedule: { + directory: [ + 'path/to/otherSchedule', + ], + }, +} as Partial; diff --git a/test/fixtures/demo/package.json b/test/fixtures/demo/package.json new file mode 100644 index 0000000..e69de29 From fc48c5947288d9d846cfb87d89cbfe0f3b3e5e87 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Tue, 17 Dec 2024 19:00:48 +0800 Subject: [PATCH 09/10] f --- test/fixtures/symlink/package.json | 4 ++++ test/schedule.test.ts | 4 ++++ 2 files changed, 8 insertions(+) create mode 100644 test/fixtures/symlink/package.json diff --git a/test/fixtures/symlink/package.json b/test/fixtures/symlink/package.json new file mode 100644 index 0000000..55e9a32 --- /dev/null +++ b/test/fixtures/symlink/package.json @@ -0,0 +1,4 @@ +{ + "name": "symlink", + "type": "module" +} diff --git a/test/schedule.test.ts b/test/schedule.test.ts index fb1b1e0..9c735c1 100644 --- a/test/schedule.test.ts +++ b/test/schedule.test.ts @@ -373,6 +373,10 @@ describe('test/schedule.test.ts', () => { }); it('should run schedule with symlink js file success', async () => { + if (!process.version.startsWith('v22.')) { + // only work on Node.js >= v22 + return; + } const realPath = path.join(__dirname, 'fixtures/symlink/realFile.js'); const targetPath = path.join(__dirname, 'fixtures/symlink/runDir/app/schedule/realFile.js'); try { From 735c21bb2d2ce762732fe3508293d089c53b7c61 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Tue, 17 Dec 2024 19:04:37 +0800 Subject: [PATCH 10/10] f --- src/lib/load_schedule.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/load_schedule.ts b/src/lib/load_schedule.ts index c9410a0..b3fc656 100644 --- a/src/lib/load_schedule.ts +++ b/src/lib/load_schedule.ts @@ -2,6 +2,7 @@ import path from 'node:path'; import assert from 'node:assert'; import { stringify } from 'node:querystring'; import { isClass, isFunction, isGeneratorFunction } from 'is-type-of'; +import { importResolve } from '@eggjs/utils'; import type { EggApplicationCore, EggContext } from 'egg'; import type { EggScheduleConfig, EggScheduleTask, EggScheduleItem } from './types.js'; @@ -42,7 +43,7 @@ function getScheduleLoader(app: EggApplicationCore) { } // handle symlink case - const realFullpath = require.resolve(fullpath); + const realFullpath = importResolve(fullpath); target[realFullpath] = { schedule: scheduleConfig, scheduleQueryString: stringify(scheduleConfig as any),