├── .gitignore ├── .php_cs ├── Dockerfile ├── README.md ├── README.zh.md ├── bin └── swoole-jobs.php ├── composer.json ├── config.php ├── docs ├── ChangeLog.md ├── README.md ├── TestingReport.md ├── example │ └── bin │ │ ├── swoole-jobs-tp5 │ │ └── swoole-jobs-yii2 ├── images │ ├── alipay-pay.jpg │ ├── demo.png │ ├── dingding.png │ ├── jobs-archi.png │ ├── jobs-process.png │ ├── status.png │ ├── testing │ │ ├── test1.png │ │ ├── test2.png │ │ └── test3.png │ └── weixin-pay.jpg └── systemd │ └── swoole-jobs.service ├── log └── .gitignore ├── phpunit.xml.dist ├── src ├── Action │ ├── ActionInterface.php │ ├── BaseAction.php │ ├── PhalconAction.php │ ├── SwooleJobsAction.php │ ├── ThinkPHP5Action.php │ ├── YafAction.php │ ├── Yii1Action.php │ └── YiiAction.php ├── Api │ ├── Controller │ │ └── Index.php │ └── Services │ │ └── PushJobs.php ├── Cache.php ├── Command │ ├── AppCommand.php │ ├── Command.php │ └── HttpCommand.php ├── Config.php ├── HttpServer.php ├── JobObject.php ├── Jobs.php ├── Jobs │ ├── DefaultClassMethod.php │ ├── MyJob.php │ └── MyJob2.php ├── Logs.php ├── Message │ ├── BaseMessage.php │ ├── DingMessage.php │ ├── Message.php │ └── MessageInterface.php ├── Process.php ├── Queue │ ├── BaseTopicQueue.php │ ├── Queue.php │ ├── RabbitmqTopicQueue.php │ ├── RedisTopicQueue.php │ └── TopicQueueInterface.php ├── Router.php ├── Serialize.php ├── TopicConfigObject.php └── Utils.php └── tests ├── DefaultJobClassMethodConfigTest.php ├── QueueTest.php ├── TopicConfigObjectTest.php ├── testCache.php ├── testJobsJson.php ├── testJobsSerialzie.php ├── testMessage.php ├── testRabbitmqConsume.php ├── testRabbitmqProducer.php └── testSerialize.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/* 2 | .DS_Store* 3 | .code-workspace 4 | log/* 5 | composer.lock 6 | src/master.pid 7 | tests/build 8 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | $header = <<<'EOF' 11 | This file is part of Swoole-jobs 12 | (c) kcloze 13 | This source file is subject to the MIT license that is bundled 14 | with this source code in the file LICENSE. 15 | EOF; 16 | 17 | return PhpCsFixer\Config::create() 18 | ->setRiskyAllowed(true) 19 | ->setRules([ 20 | '@Symfony' => true, 21 | '@Symfony:risky' => true, 22 | 'array_syntax' => ['syntax' => 'short'], 23 | 'combine_consecutive_unsets' => true, 24 | // one should use PHPUnit methods to set up expected exception instead of annotations 25 | 'general_phpdoc_annotation_remove' => ['expectedException', 'expectedExceptionMessage', 'expectedExceptionMessageRegExp'], 26 | //'header_comment' => ['header' => $header], 27 | //'heredoc_to_nowdoc' => true, 28 | 'no_extra_consecutive_blank_lines' => ['break', 'continue', 'extra', 'return', 'throw', 'use', 'parenthesis_brace_block', 'square_brace_block', 'curly_brace_block'], 29 | 'no_unreachable_default_argument_value' => true, 30 | 'no_useless_else' => true, 31 | 'no_useless_return' => true, 32 | 'ordered_class_elements' => true, 33 | 'ordered_imports' => true, 34 | 'php_unit_strict' => true, 35 | 'phpdoc_add_missing_param_annotation' => true, 36 | 'phpdoc_order' => true, 37 | 'psr4' => true, 38 | 'strict_comparison' => false, 39 | 'strict_param' => true, 40 | 'binary_operator_spaces' => ['align_double_arrow' => true, 'align_equals' => true], 41 | 'concat_space' => ['spacing' => 'one'], 42 | 'no_empty_statement' => true, 43 | 'simplified_null_return' => true, 44 | 'no_extra_consecutive_blank_lines' => true, 45 | 'pre_increment' => false, 46 | ]) 47 | ->setFinder( 48 | PhpCsFixer\Finder::create() 49 | ->exclude('vendor') 50 | ->in(__DIR__) 51 | ) 52 | ->setUsingCache(false) 53 | ; 54 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # FROM phpswoole/swoole:4.8-php7.4-alpine 2 | FROM phpswoole/swoole:latest 3 | 4 | LABEL maintainer="Swoole Jobs " version="1.1" license="MIT" 5 | 6 | 7 | 8 | RUN docker-php-ext-install amqp 9 | 10 | RUN printf "# php cli ini settings\n\ 11 | date.timezone=PRC\n\ 12 | memory_limit=-1\n\ 13 | " >> /usr/local/etc/php/conf.d/php.ini 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # swoole-jobs 2 | 3 | ## [中文介绍](https://github.com/kcloze/swoole-jobs/blob/master/README.zh.md) 4 | 5 | 6 | * Distributed task processing system,similar to gearman,based on swoole 7 | * High performance / dynamic multi woker process consumption queue to accelerate backend time consuming service 8 | * There is no need to configure a crontab like gearman worker, swoole-jobs is responsible for managing all worker states 9 | * Support for pushing queues by HTTP API(swoole http server) , does not depend on php-fpm 10 | 11 | 12 | 13 | 14 | ## 1. Explain 15 | 16 | * Slower logic in web, such as statistical /email/ SMS / picture processing, etc. 17 | * Support redis/rabbitmq/zeromq or any other queue message store. 18 | * It is more stable and faster than the Yii / laravel framework itself. 19 | * With yii2/phalcon/yaf/ThinkPHP5 integration example, other frameworks can refer to src/Action code. 20 | * [yii2 demo](https://github.com/kcloze/swoole-jobs-yii2) 21 | * [ThinkPHP5 demo](https://github.com/kcloze/swoole-jobs-tp5) 22 | 23 | 24 | ## 2. Architecture diagram 25 | 26 | ![Architecture diagram](docs/images/jobs-archi.png) 27 | ![Process model](docs/images/jobs-process.png) 28 | 29 | 30 | ## 3. Characteristic 31 | 32 | * job scheduling component based on swoole; distributed task processing system similar to gearman; 33 | 34 | * redis/rabbitmq/zeromq and any other queue message store (currently only redis/rabbitmq). 35 | 36 | * use swoole process to realize multi process management, the number of processes can be configured, and the worker process will automatically pull up after exiting. 37 | 38 | * the number of cycles of child processes can be configured to prevent memory leakage from business code; the default stop command will wait for the child process to exit smoothly. 39 | 40 | * support topic features, different job binding different topic; 41 | 42 | * each topic starts the corresponding number of sub processes to eliminate the interaction between different topic. 43 | 44 | * according to the queue backlog, the sub process starts the process dynamically, and the number of the largest sub processes can be configured. 45 | 46 | * support composer, which can be integrated with any framework; 47 | 48 | * log file automatic cutting, default maximum 100M, up to 5 log files, prevent log brush full disk; 49 | 50 | * backlog, support for nail robot and other news alerts. 51 | 52 | 53 | ## 4. Install 54 | 55 | #### 4.1 composer 56 | ``` 57 | git clone https://github.com/kcloze/swoole-jobs.git 58 | cd swoole-jobs 59 | 60 | ``` 61 | 62 | 63 | ``` 64 | composer install 65 | ``` 66 | #### 4.2 docker 67 | * git clone https://github.com/kcloze/swoole-jobs.git 68 | * cd swoole-jobs and composer install 69 | * Building a mirror based on the root directory Dockerfile 70 | * docker build -t swoole-jobs . 71 | * docker run -it -v ~/data/code/php:/data swoole-jobs /bin/bash 72 | * After entering the docker container, enter the project directory: 73 | * `php ./bin/swoole-jobs.php start` 74 | 75 | ## 5. How to running 76 | 77 | ### 5.1 example 78 | ``` 79 | 1.edit config.php 80 | 81 | 2.start service 82 | php ./bin/swoole-jobs.php start >> log/system.log 2>&1 83 | 84 | 3.push jobs 85 | php ./tests/testJobsSerialzie.php 86 | 87 | 4.start api server 88 | php ./bin/swoole-jobs.php http start 89 | 90 | 5.stop api server 91 | php ./bin/swoole-jobs.php http stop 92 | ``` 93 | 94 | ### 5.2 Start parameter description 95 | ``` 96 | NAME 97 | - manage swoole-jobs 98 | 99 | SYNOPSIS 100 | -php bin/swoole-jobs.php app [options] 101 | -Manage swoole-jobs daemons. 102 | 103 | WORKFLOWS 104 | 105 | -help [command] 106 | -Show this help, or workflow help for command. 107 | 108 | -restart 109 | -Stop, then start swoole-jobs master and workers. 110 | 111 | -start 112 | -Start swoole-jobs master and workers. 113 | 114 | -stop 115 | -Wait all running workers smooth exit, please check swoole-jobs status for a while. 116 | 117 | -exit 118 | -Kill all running workers and master PIDs. 119 | 120 | -http start 121 | -Start swoole http server for apis. 122 | 123 | -http stop 124 | -Stop swoole http server for api. 125 | 126 | ``` 127 | ### 5.3 API parameter description 128 | 129 | #### 5.3.1 api url 130 | * http://localhost:9501/pushJobs 131 | 132 | #### 5.3.2 api params: 133 | 134 | | Params | Type | Demo | 135 | | ------------- |:-------------:| -----:| 136 | | jobData | json | {"topic":"MyJob","jobClass":"\\Kcloze\\Jobs\\Jobs\\MyJob","jobMethod":"test2","jobParams":["kcloze",1532857253,"oop"],"jobExtras":[],"serializeFunc":"php"} | 137 | 138 | ## 6. Service management 139 | ### There are two ways to start and close the service online: 140 | 141 | #### 6.1 The startup script is added to the crontab timing task, which is executed once a minute (swoole-jobs automatically checks if it is executing, avoiding repeated startup). 142 | 143 | ``` 144 | * * * * * /usr/local/bin/php /***/swoole-jobs.php start >> /***/log/system.log 2>&1 145 | 146 | ``` 147 | 148 | 149 | 150 | #### 6.2 Using SYSTEMd Management (failure restart, boot up) 151 | [more](https://www.swoole.com/wiki/page/699.html) 152 | 153 | ``` 154 | 1. According to your own project path, modify: docs/systemd/swoole-jobs.service 155 | 2. sudo cp -f systemd/swoole-jobs.service /etc/systemd/system/ 156 | 3. sudo systemctl --system daemon-reload 157 | 4. Service management 158 | 159 | ``` 160 | #start service 161 | sudo systemctl start swoole-jobs.service 162 | #reload service 163 | sudo systemctl reload swoole-jobs.service 164 | #stop service 165 | sudo systemctl stop swoole-jobs.service 166 | 167 | 168 | ## 7.System screenshot 169 | #### htop 170 | ![demo](docs/images/demo.png) 171 | #### status 172 | ![status](docs/images/status.png) 173 | #### dingding message 174 | ![message](docs/images/dingding.png) 175 | 176 | 177 | 178 | ## 8. Change log 179 | * [change log](docs/ChangeLog.md) 180 | 181 | ## 9. Matters needing attention 182 | * If you embed your own framework, you can refer to src/Action code to inherit the abstract class Kcloze\Jobs\Action\BaseAction. 183 | * Various framework services will start slightly different, for specific reference: Code for `example/bin` projects. 184 | * swoole >=4 version,need to set php.ini, [disable coroutine ](https://github.com/swoole/swoole-src/issues/2716) 185 | 186 | ## 10. Pressure measurement 187 | * Bottleneck: redis/rabbitmq queue storage itself and job execution speed 188 | 189 | ## 11. Thanks 190 | * [swoole](http://www.swoole.com/) 191 | 192 | ## 12. Contact 193 | qq group:141059677 194 | 195 | 196 | ## 13. Donation 197 | * If this project really helps you, please click on the top right corner for a star. 198 | 199 | 200 | 201 | 202 | -------------------------------------------------------------------------------- /README.zh.md: -------------------------------------------------------------------------------- 1 | # swoole-jobs 2 | 3 | * 基于swoole类似gearman的分布式任务处理系统 4 | * 高性能/动态多woker进程消费队列,加速后端耗时服 5 | * 无需像gearman一个worker配置一条crontab,swoole-jobs负责管理所有worker状态 6 | * 独立的swoole http api 入队列,api不依赖php-fpm 7 | 8 | 9 | ## 1. 说明 10 | 11 | * web中较慢的逻辑,比如统计/email/短信/图片处理等; 12 | * 支持redis/rabbitmq/zeromq等任何一种做队列消息存储; 13 | * 比yii/laravel等框架自带队列更稳定更快[消费进程可动态变化] 14 | * 自带yii2/phalcon/yaf/ThinkPHP5集成示例,其他框架可参考src/Action代码, 15 | * [yii2完整示例](https://github.com/kcloze/swoole-jobs-yii2) 16 | * [ThinkPHP5完整示例](https://github.com/kcloze/swoole-jobs-tp5) 17 | 18 | 19 | ## 2. 架构图 20 | 21 | ![架构图](docs/images/jobs-archi.png) 22 | ![进程模型](docs/images/jobs-process.png) 23 | 24 | 25 | ## 3. 特性 26 | 27 | * 基于swoole的job调度组件;类似gearman的分布式任务处理系统; 28 | * redis/rabbitmq/zeromq等任何一种做队列消息存储(目前只实现redis/rabbitmq); 29 | * 利用swoole的process实现多进程管理,进程个数可配置,worker进程退出后会自动拉起; 30 | * 子进程循环次数可配置,防止业务代码内存泄漏;默认stop命令会等待子进程平滑退出; 31 | * 支持topic特性,不同的job绑定不同的topic; 32 | * 每个topic启动对应数量的子进程,杜绝不同topic之间相互影响; 33 | * 根据队列积压情况,子进程动态启动进程数,最大子进程个数可配置; 34 | * 支持composer,可以跟任意框架集成; 35 | * 日志文件自动切割,默认最大100M,最多5个日志文件,防止日志刷满磁盘; 36 | * 出现积压情况,支持钉钉机器人等消息提醒; 37 | 38 | 39 | ## 4. 安装 40 | 41 | #### 4.1 composer 42 | ``` 43 | git clone https://github.com/kcloze/swoole-jobs.git 44 | cd swoole-jobs 45 | 46 | ``` 47 | 48 | 49 | ``` 50 | composer install 51 | ``` 52 | #### 4.2 docker 53 | * git clone https://github.com/kcloze/swoole-jobs.git 54 | * cd swoole-jobs and composer install 55 | * 根据根目录Dockerfile构建镜像 56 | * docker build -t swoole-jobs . 57 | * docker run -it -v ~/data/code/php:/data swoole-jobs /bin/bash 58 | * 进入容器之后,进入项目目录: 59 | * `php ./bin/swoole-jobs.php start` 60 | 61 | ## 5. 运行 62 | 63 | ### 5.1 示范 64 | ``` 65 | 1.修改配置config.php 66 | 67 | 2.启动服务 68 | php ./bin/swoole-jobs.php start >> log/system.log 2>&1 69 | 70 | 3.往队列推送任务 71 | php ./tests/testJobsSerialzie.php 72 | 73 | 4.启动api服务 74 | php ./bin/swoole-jobs.php http start 75 | 76 | 5.停止api服务 77 | php ./bin/swoole-jobs.php http stop 78 | 79 | 80 | 81 | ``` 82 | 83 | ### 5.2 启动参数说明 84 | ``` 85 | NAME 86 | - manage swoole-jobs 87 | 88 | SYNOPSIS 89 | -php ./bin/swoole-jobs.php app [options] 90 | -Manage swoole-jobs daemons. 91 | 92 | WORKFLOWS 93 | 94 | -help [command] 95 | -Show this help, or workflow help for command. 96 | 97 | -restart 98 | -Stop, then start swoole-jobs master and workers. 99 | 100 | -start 101 | -Start swoole-jobs master and workers. 102 | 103 | -stop 104 | -Wait all running workers smooth exit, please check swoole-jobs status for a while. 105 | 106 | -exit 107 | -Kill all running workers and master PIDs. 108 | 109 | -http start 110 | -Start swoole http server for apis. 111 | 112 | -http stop 113 | -Stop swoole http server for api. 114 | 115 | 116 | ``` 117 | 118 | ### 5.3 API parameter description 119 | 120 | #### 5.3.1 api url 121 | * http://localhost:9501/pushJobs 122 | 123 | #### 5.3.2 api params: 124 | 125 | | Params | Type | Demo | 126 | | ------------- |:-------------:| -----:| 127 | | jobData | json | {"topic":"MyJob","jobClass":"\\Kcloze\\Jobs\\Jobs\\MyJob","jobMethod":"test2","jobParams":["kcloze",1532857253,"oop"],"jobExtras":[],"serializeFunc":"php"} | 128 | 129 | 130 | ## 6. 服务管理 131 | ### 线上启动和关闭服务,有两种方式: 132 | 133 | #### 6.1 启动脚本加入到crontab定时任务,每分钟执行一次(swoole-jobs会自动检查是否在执行,避免重复启动) 134 | 135 | ``` 136 | * * * * * /usr/local/bin/php /***/swoole-jobs.php start >> /***/log/system.log 2>&1 137 | 138 | ``` 139 | 140 | 141 | 142 | #### 6.2 使用systemd管理(故障重启、开机自启动) 143 | [更多systemd介绍](https://www.swoole.com/wiki/page/699.html) 144 | 145 | ``` 146 | 1. 根据自己项目路径,修改 docs/systemd/swoole-jobs.service 147 | 2. sudo cp -f systemd/swoole-jobs.service /etc/systemd/system/ 148 | 3. sudo systemctl --system daemon-reload 149 | 4. 服务管理 150 | #启动服务 151 | sudo systemctl start swoole-jobs.service 152 | #reload服务 153 | sudo systemctl reload swoole-jobs.service 154 | #关闭服务 155 | sudo systemctl stop swoole-jobs.service 156 | 157 | ``` 158 | 159 | ## 7.系统截图 160 | #### htop截图 161 | ![实例图](docs/images/demo.png) 162 | #### status 163 | ![status](docs/images/status.png) 164 | #### 钉钉提醒 165 | ![message](docs/images/dingding.png) 166 | 167 | 168 | 169 | 170 | ## 8. change log 171 | * [change log](docs/ChangeLog.md) 172 | 173 | ## 9. 注意事项 174 | * 如果嵌入自己的框架,可参考src/Action代码,继承抽象类Kcloze\Jobs\Action\BaseAction 175 | * 各种框架服务启动会稍有不同,具体参考:`example/bin`项目的代码 176 | * swoole 4 以上版本需要设置php.ini,[关闭协程 ](https://github.com/swoole/swoole-src/issues/2716) 177 | 178 | ## 10. 压测 179 | * 瓶颈: redis/rabbitmq队列存储本身和job执行速度 180 | 181 | ## 11. 感谢 182 | * [swoole](http://www.swoole.com/) 183 | 184 | ## 12. 联系 185 | qq群:141059677 186 | 187 | 188 | ## 13. 捐赠 189 | * 如果这个项目真的帮助到你,麻烦点击右上角给个star 190 | 191 | 192 | 193 | 194 | -------------------------------------------------------------------------------- /bin/swoole-jobs.php: -------------------------------------------------------------------------------- 1 | 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | define('SWOOLE_JOBS_ROOT_PATH', dirname(__DIR__)); 11 | 12 | use Kcloze\Jobs\Command\AppCommand; 13 | use Kcloze\Jobs\Command\HttpCommand; 14 | use Symfony\Component\Console\Application; 15 | 16 | require SWOOLE_JOBS_ROOT_PATH . '/vendor/autoload.php'; 17 | $config = require_once SWOOLE_JOBS_ROOT_PATH . '/config.php'; 18 | 19 | $application = new Application(); 20 | $appCommand = new AppCommand($config); 21 | $application->add($appCommand); 22 | 23 | //check if it has http command 24 | $option=$argv[1] ?? ''; 25 | if (isset($config['httpServer']) && $option==='http') { 26 | $httpCommand = new HttpCommand($config); 27 | $application->add($httpCommand); 28 | $application->setDefaultCommand($appCommand->getName()); 29 | } else { 30 | $application->setDefaultCommand($appCommand->getName(), true); 31 | } 32 | 33 | $application->run(); 34 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kcloze/swoole-jobs", 3 | "description": "基于swoole的job调度组件,类似gearman的分布式任务处理系统", 4 | "keywords": ["swoole", "job", "scheduler", "调度"], 5 | "homepage": "https://github.com/kcloze/swoole-jobs", 6 | "license": "MIT", 7 | "require": { 8 | "php": ">=8.0", 9 | "ext-swoole": ">=4.8", 10 | "ext-redis": "*", 11 | "symfony/console": "^6.1", 12 | "bramus/router": "^1.6", 13 | "guzzlehttp/guzzle": "^7.5" 14 | 15 | }, 16 | "require-dev": { 17 | "phpunit/phpunit": "^4.8.35 || ^5.7" 18 | }, 19 | "authors": [ 20 | { 21 | "name": "kcloze", 22 | "email": "pei.greet@qq.com" 23 | } 24 | ], 25 | "autoload": { 26 | "psr-4": { 27 | "Kcloze\\Jobs\\": "src" 28 | } 29 | }, 30 | "bin": [ 31 | "swoole-jobs" 32 | ] 33 | 34 | } 35 | -------------------------------------------------------------------------------- /config.php: -------------------------------------------------------------------------------- 1 | 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | date_default_timezone_set('Asia/Shanghai'); 11 | 12 | return $config = [ 13 | //项目/系统标识 14 | 'system' => 'swoole-jobs', 15 | //log目录 16 | 'logPath' => __DIR__ . '/log', 17 | 'logSaveFileApp' => 'application.log', //默认log存储名字 18 | 'logSaveFileWorker' => 'crontab.log', // 进程启动相关log存储名字 19 | 'pidPath' => __DIR__ . '/log', 20 | 'sleep' => 2, // 队列没消息时,暂停秒数 21 | 'queueMaxNum' => 10, // 队列达到一定长度,发送消息提醒 22 | 'queueMaxNumForProcess' => 10, // 队列达到一定长度,启动动态子进程 23 | 'maxPopNum' => 50, //子进程最多执行任务数,达到这个数量之后,自动退出 24 | 'excuteTime' => 600, // 子进程最长执行时间,防止内存泄漏 25 | 'queueTickTimer' => 1000 * 15, //一定时间间隔(毫秒)检查队列长度;默认10秒钟 26 | 'messageTickTimer' => 1000 * 180, //一定时间间隔(毫秒)发送消息提醒;默认3分钟 27 | 'processName' => ':swooleTopicQueue', // 设置进程名, 方便管理, 默认值 swooleTopicQueue 28 | //'eachJobExit' => false, // true 开启; false 关闭;每个job执行完之后,主动exit,防止业务代码出现(正常不需要开启) 29 | 30 | //job任务相关 31 | 'job' => [ 32 | //job相关属性 33 | 'profile'=> [ 34 | 'maxTime'=> 3, //单个job最大执行时间 35 | 'minTime'=> 0.0001, //单个job最少执行时间 36 | ], 37 | 'topics' => [ 38 | //'autoAckBeforeJobStart' => true, // true 开启; false 关闭;默认为true,job没跑之前是否直接ack,这样业务代码里面有exit、die等致命错误会丢弃消息,防止消息积压 39 | ['name'=>'MyJob', 'workerMinNum'=>1, 'workerMaxNum'=>3, 'queueMaxNum'=>10000, 'queueMaxNumForProcess' => 100, 'autoAckBeforeJobStart'=>true], 40 | ['name'=> 'MyJob2', 'workerMinNum'=>1, 'workerMaxNum'=>3, 'autoAckBeforeJobStart'=>true], 41 | ['name'=> 'MyJob3', 'workerMinNum'=>1, 'workerMaxNum'=>1], 42 | ['name'=> 'DefaultClassMethod.test1', 'workerMinNum'=>1, 'workerMaxNum'=>2, 'defaultJobClass'=>'DefaultClassMethod', 'defaultJobMethod'=>'test1'], 43 | ['name'=> 'DefaultClassMethod.test2', 'workerMinNum'=>1, 'workerMaxNum'=>2, 'defaultJobClass'=>'DefaultClassMethod', 'defaultJobMethod'=>'test2'], 44 | //不需要swoole-jobs消费的队列,只往队列里面写数据 45 | //['name'=> 'TojavaConsumer'], 46 | ], 47 | // redis 48 | // 'queue' => [ 49 | // 'class' => '\Kcloze\Jobs\Queue\RedisTopicQueue', 50 | // 'host' => '127.0.0.1', 51 | // 'port' => 6379, 52 | // //'password'=> 'pwd', 53 | // ], 54 | 55 | // rabbitmq 56 | 'queue' => [ 57 | 'class' => '\Kcloze\Jobs\Queue\RabbitmqTopicQueue', 58 | 'host' => '192.168.3.9', 59 | 'user' => 'guest', 60 | 'pass' => 'guest', 61 | 'port' => '5672', 62 | 'vhost' => 'php', 63 | 'exchange' => 'php.amqp.ext', 64 | ], 65 | ], 66 | 67 | //框架类型及装载类 68 | 'framework' => [ 69 | //可以自定义,但是该类必须继承\Kcloze\Jobs\Action\BaseAction 70 | 'class'=> '\Kcloze\Jobs\Action\SwooleJobsAction', 71 | ], 72 | 'message'=> [ 73 | 'class' => '\Kcloze\Jobs\Message\DingMessage', 74 | 'token' => '**', 75 | ], 76 | 'httpServer' => [ 77 | 'host' => '0.0.0.0', 78 | 'port' => 9502, 79 | 'settings'=> [ 80 | 'worker_num' => 3, 81 | 'daemonize' => true, 82 | //'max_request' => 1, 83 | 'dispatch_mode' => 2, 84 | 'pid_file' => __DIR__ . '/log/server.pid', 85 | 'log_file' => __DIR__ . '/log/server.log', 86 | ], 87 | ], 88 | ]; 89 | -------------------------------------------------------------------------------- /docs/ChangeLog.md: -------------------------------------------------------------------------------- 1 | 2 | # Change Log 3 | 4 | #### 2018-1-30 v2.5.3 子进程pop队列个数和最大执行时间可配置 5 | * 1.可配置参数:子进程pop队列个数,队列job个数临界值(启动动态进程和消息提醒),最大执行时间; 6 | * 2.修复workerMinNum和workerMaxNum相同时,不启动动态worker进程; 7 | 8 | 9 | #### 2018-1-9 v2.4.7发布,子进程改为执行一段时间再退出,提升性能 10 | 11 | * 1.之前子进程退出过于频繁,现在子进程改为一小时再退出,while死循环执行队列任务,直到超出一小时最大执行时间; 12 | * 2.避免子进程频繁创建和退出,提高性能; 13 | * 3.详情参考代码:https://github.com/kcloze/swoole-jobs/blob/master/src/Process.php#L130 14 | 15 | #### 2017-12-31 16 | * 出现积压队列情况,支持钉钉机器人等消息提醒; 17 | * 支持status状态显示命令; 18 | * 静态子进程可能重启失败,增强日志记录; 19 | 20 | #### 2017-12-10 21 | * worker子进程支持个数可以根据队列挤压情况动态变化; 22 | * worker子进程最大和最小启动个数可配置; 23 | 24 | #### 2017-12-4 25 | * 子进程启动模式变更:单独给每个topic启动对应数量的子进程,杜绝不同topic之间相互影响 26 | 27 | #### 2017-11-30 28 | * 增加exit启动参数,默认stop等待子进程平滑退出 29 | 30 | #### 2017-11-29 31 | * 重构自身和第三方框架装载类实现,降低耦合性; 32 | * 支持Yii和Phalcon主流框架 33 | 34 | #### 2017-11-28 16:52:42 35 | * topics支持根据key值排序,队列根据这个排序优先消费; 36 | * 优化启动流程,让PHP进程自身管理,移除服务管理脚本; 37 | * 重构代码,优化结构; 38 | 39 | #### 2017-11-28 00:27:42 40 | 41 | > by [daydaygo](http://github.com/daydaygo) 42 | - 优化 TopicQueue 实现: `TopicQueueInterface -> BaseTopicQueue -> XxxTopicQueue` 43 | - 优化 job run() 方式, 增加类静态方法实现, 并实现多参数输入 44 | - 使用依赖注入方式进行解耦, 比如 `Jobs` 类依赖 `BaseTopicQueue` 抽象类, 不和具体的 `TopicQueue` 实现耦合; 比如配置的解耦, `Jobs` 类只用关系自己业务相关的配置, 不用耦合其他配置 45 | - 添加 php 进行服务管理 46 | 47 | #### 2017-5-19 48 | * 增加使用systemd管理swoole服务,实现故障重启、开机自启动等功能 -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # swoole-jobs 操作手册 2 | 3 | ## 目录 4 | 5 | ## 介绍 6 | 7 | #### [安装与配置](Install.md) 8 | 9 | #### [入门指导](Startup.md) 10 | 11 | #### [开发指南](Develop.md) 12 | 13 | #### [测试报告](TestingReport.md) -------------------------------------------------------------------------------- /docs/TestingReport.md: -------------------------------------------------------------------------------- 1 | # Testing Report 2 | 3 | #### 环境准备 4 | 5 | #### 系统信息 6 | * 系统:CentOS release 6.6 (Final) 7 | * cpu: 4 QEMU Virtual CPU version (cpu64-rhel6) 8 | * 内存:6 G 9 | 10 | #### swoole-jobs配置 11 | * 队列类型:rabbitmq 12 | * 队列插入数量 MyJob 13 | 14 | | topic名 | 插入个数 | 描述 | 最少消费进程数 | 最大消费进程数 | 15 | | ----------- | --------| --------------- | ---- | ------ | 16 | | MyJob | 10万 | MyJob | 3 | 30 | 17 | | MyJob2 | 10万 | MyJob2 | 3 | 20 | 18 | | MyJob3 | 0 | MyJob2 | 1 | 1 | 19 | 20 | 21 | 22 | 23 | * topic相关 24 | ``` 25 | 'topics' => [ 26 | ['name'=>'MyJob', 'workerMinNum'=>3, 'workerMaxNum'=>30], 27 | ['name'=> 'MyJob2', 'workerMinNum'=>3, 'workerMaxNum'=>20], 28 | ['name'=> 'MyJob3', 'workerMinNum'=>1, 'workerMaxNum'=>1], 29 | ], 30 | ``` 31 | 32 | * 子进程相关 33 | ``` 34 | 'sleep' => 2, // 队列没消息时,暂停秒数 35 | 'queueMaxNum' => 10, // 队列达到一定长度,启动动态子进程个数发和送消息提醒 36 | 'maxPopNum' => 100, // 子进程启动后每个循环最多取多少个job 37 | 'excuteTime' => 3600, // 子进程最长执行时间,防止内存泄漏 38 | ``` 39 | 40 | 41 | #### 系统表现 42 | 43 | * 任务开始状态 44 | ![任务开始状态](images/testing/test1.png) 45 | 46 | * 10分钟之后状态 47 | ![任务开始状态](images/testing/test2.png) 48 | 49 | * 30分钟之后状态 50 | ![任务开始状态](images/testing/test3.png) 51 | 52 | 53 | 54 | #### 结论 -------------------------------------------------------------------------------- /docs/example/bin/swoole-jobs-tp5: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | // 定义项目路径 10 | 11 | define('APP_PATH', __DIR__ . '/application/'); 12 | define('RUNTIME_PATH', __DIR__ . '/runtime/'); 13 | define('SWOOLE_JOBS_ROOT_PATH', __DIR__); 14 | date_default_timezone_set('Asia/Shanghai'); 15 | 16 | // ThinkPHP 引导文件 17 | require SWOOLE_JOBS_ROOT_PATH . '/thinkphp/base.php'; 18 | require SWOOLE_JOBS_ROOT_PATH . '/vendor/autoload.php'; 19 | $config = require_once SWOOLE_JOBS_ROOT_PATH . '/application/swoole-jobs.php'; 20 | 21 | $console = new \Kcloze\Jobs\Console($config); 22 | $console->run(); 23 | -------------------------------------------------------------------------------- /docs/example/bin/swoole-jobs-yii2: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 7 | * This source file is subject to the MIT license that is bundled 8 | * with this source code in the file LICENSE. 9 | */ 10 | define('SWOOLE_JOBS_ROOT_PATH', __DIR__); 11 | 12 | date_default_timezone_set('Asia/Shanghai'); 13 | 14 | require SWOOLE_JOBS_ROOT_PATH . '/vendor/autoload.php'; 15 | require SWOOLE_JOBS_ROOT_PATH . '/vendor/yiisoft/yii2/Yii.php'; 16 | 17 | $config = require_once SWOOLE_JOBS_ROOT_PATH . '/config/swoole-jobs.php'; 18 | 19 | $console = new Kcloze\Jobs\Console($config); 20 | $console->run(); 21 | -------------------------------------------------------------------------------- /docs/images/alipay-pay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kcloze/swoole-jobs/c50b5ea5f663186bf86151d8cfae4ec4a231beda/docs/images/alipay-pay.jpg -------------------------------------------------------------------------------- /docs/images/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kcloze/swoole-jobs/c50b5ea5f663186bf86151d8cfae4ec4a231beda/docs/images/demo.png -------------------------------------------------------------------------------- /docs/images/dingding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kcloze/swoole-jobs/c50b5ea5f663186bf86151d8cfae4ec4a231beda/docs/images/dingding.png -------------------------------------------------------------------------------- /docs/images/jobs-archi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kcloze/swoole-jobs/c50b5ea5f663186bf86151d8cfae4ec4a231beda/docs/images/jobs-archi.png -------------------------------------------------------------------------------- /docs/images/jobs-process.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kcloze/swoole-jobs/c50b5ea5f663186bf86151d8cfae4ec4a231beda/docs/images/jobs-process.png -------------------------------------------------------------------------------- /docs/images/status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kcloze/swoole-jobs/c50b5ea5f663186bf86151d8cfae4ec4a231beda/docs/images/status.png -------------------------------------------------------------------------------- /docs/images/testing/test1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kcloze/swoole-jobs/c50b5ea5f663186bf86151d8cfae4ec4a231beda/docs/images/testing/test1.png -------------------------------------------------------------------------------- /docs/images/testing/test2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kcloze/swoole-jobs/c50b5ea5f663186bf86151d8cfae4ec4a231beda/docs/images/testing/test2.png -------------------------------------------------------------------------------- /docs/images/testing/test3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kcloze/swoole-jobs/c50b5ea5f663186bf86151d8cfae4ec4a231beda/docs/images/testing/test3.png -------------------------------------------------------------------------------- /docs/images/weixin-pay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kcloze/swoole-jobs/c50b5ea5f663186bf86151d8cfae4ec4a231beda/docs/images/weixin-pay.jpg -------------------------------------------------------------------------------- /docs/systemd/swoole-jobs.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Swoole Jobs Server 3 | After=network.target 4 | After=syslog.target 5 | 6 | [Service] 7 | Type=forking 8 | PIDFile=/media/kcloze/8685937c-af42-4319-aa9b-bb123ccd18ba/data/www/kcloze/swoole-jobs/log/master.pid 9 | ExecStart=/usr/bin/php7.0 /media/kcloze/8685937c-af42-4319-aa9b-bb123ccd18ba/data/www/kcloze/swoole-jobs/run.php start >> /media/kcloze/8685937c-af42-4319-aa9b-bb123ccd18ba/data/www/kcloze/swoole-jobs/log/server.log 2>&1 10 | ExecStop=/bin/kill $MAINPID 11 | ExecReload=/bin/kill -USR1 $MAINPID 12 | Restart=always 13 | 14 | [Install] 15 | WantedBy=multi-user.target graphical.target -------------------------------------------------------------------------------- /log/.gitignore: -------------------------------------------------------------------------------- 1 | * -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | tests 15 | 16 | 17 | 18 | 19 | src/ 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/Action/ActionInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Kcloze\Jobs\Action; 11 | 12 | use Kcloze\Jobs\JobObject; 13 | 14 | interface ActionInterface 15 | { 16 | public function init(); 17 | 18 | public function start(JobObject $JobObject); 19 | } 20 | -------------------------------------------------------------------------------- /src/Action/BaseAction.php: -------------------------------------------------------------------------------- 1 | 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Kcloze\Jobs\Action; 11 | 12 | use Kcloze\Jobs\JobObject; 13 | 14 | abstract class BaseAction 15 | { 16 | public function init() 17 | { 18 | } 19 | 20 | public function start(JobObject $JobObject) 21 | { 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Action/PhalconAction.php: -------------------------------------------------------------------------------- 1 | 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Kcloze\Jobs\Action; 11 | 12 | use Kcloze\Jobs\Config; 13 | use Kcloze\Jobs\JobObject; 14 | use Kcloze\Jobs\Logs; 15 | use Kcloze\Jobs\Utils; 16 | use Phalcon\Cli\Console as ConsoleApp; 17 | 18 | class PhalconAction 19 | { 20 | public function init() 21 | { 22 | $this->logger = Logs::getLogger(Config::getConfig()['logPath'] ?? '', Config::getConfig()['logSaveFileApp'] ?? '', Config::getConfig()['system'] ?? ''); 23 | } 24 | 25 | public function start(JobObject $JobObject) 26 | { 27 | $this->init(); 28 | try { 29 | $arguments['task'] =$JobObject->jobClass; 30 | $arguments['action']=$JobObject->jobMethod; 31 | $arguments['params']=$JobObject->jobParams; 32 | 33 | $config = include SWOOLE_JOBS_ROOT_PATH . '/../ycf_config/' . YII_ENV_APP_NAME . '/config.php'; 34 | include SWOOLE_JOBS_ROOT_PATH . '/app/config/loader.php'; 35 | include SWOOLE_JOBS_ROOT_PATH . '/app/config/mainCli.php'; 36 | 37 | $console = new ConsoleApp($di); 38 | $console->handle($arguments); 39 | $console->logger->flush(); 40 | } catch (\Throwable $e) { 41 | Utils::catchError($this->logger, $e); 42 | } catch (\Exception $e) { 43 | Utils::catchError($this->logger, $e); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Action/SwooleJobsAction.php: -------------------------------------------------------------------------------- 1 | 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Kcloze\Jobs\Action; 11 | 12 | use Kcloze\Jobs\Config; 13 | use Kcloze\Jobs\JobObject; 14 | use Kcloze\Jobs\Logs; 15 | use Kcloze\Jobs\Utils; 16 | 17 | class SwooleJobsAction extends BaseAction 18 | { 19 | private $logger=null; 20 | 21 | public function init() 22 | { 23 | $this->logger = Logs::getLogger(Config::getConfig()['logPath'] ?? '', Config::getConfig()['logSaveFileApp'] ?? '', Config::getConfig()['system'] ?? ''); 24 | } 25 | 26 | public function start(JobObject $JobObject) 27 | { 28 | try { 29 | $this->init(); 30 | $jobClass =$JobObject->jobClass; 31 | $jobMethod=$JobObject->jobMethod; 32 | $jobParams=$JobObject->jobParams; 33 | 34 | $obj=new $jobClass(); 35 | if (is_object($obj) && method_exists($obj, $jobMethod)) { 36 | call_user_func_array([$obj, $jobMethod], $jobParams); 37 | } else { 38 | $this->logger->log('Action obj not find: ' . json_encode($JobObject), 'error'); 39 | } 40 | } catch (\Throwable $e) { 41 | Utils::catchError($this->logger, $e); 42 | } catch (\Exception $e) { 43 | Utils::catchError($this->logger, $e); 44 | } 45 | 46 | $this->logger->log('Action has been done, action content: ' . json_encode($JobObject)); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Action/ThinkPHP5Action.php: -------------------------------------------------------------------------------- 1 | 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Kcloze\Jobs\Action; 11 | 12 | use Kcloze\Jobs\Config; 13 | use Kcloze\Jobs\JobObject; 14 | use Kcloze\Jobs\Logs; 15 | use Kcloze\Jobs\Utils; 16 | 17 | class ThinkPHP5Action 18 | { 19 | public function init() 20 | { 21 | $this->logger = Logs::getLogger(Config::getConfig()['logPath'] ?? '', Config::getConfig()['logSaveFileApp'] ?? '', Config::getConfig()['system'] ?? ''); 22 | } 23 | 24 | public function start(JobObject $JobObject) 25 | { 26 | $this->init(); 27 | try { 28 | 29 | \think\App::initCommon(); 30 | // 执行应用 31 | \think\Console::call($JobObject->jobClass,$JobObject->jobParams); 32 | } catch (\Throwable $e) { 33 | Utils::catchError($this->logger, $e); 34 | } catch (\Exception $e) { 35 | Utils::catchError($this->logger, $e); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Action/YafAction.php: -------------------------------------------------------------------------------- 1 | 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Kcloze\Jobs\Action; 11 | 12 | use Kcloze\Jobs\Config; 13 | use Kcloze\Jobs\JobObject; 14 | use Kcloze\Jobs\Logs; 15 | use Kcloze\Jobs\Utils; 16 | 17 | class YafAction 18 | { 19 | private $logger=null; 20 | 21 | private static $application =null; 22 | 23 | public function init() 24 | { 25 | $this->logger = Logs::getLogger(Config::getConfig()['logPath'] ?? '', Config::getConfig()['logSaveFileApp'] ?? '', Config::getConfig()['system'] ?? ''); 26 | } 27 | 28 | //yaf运行参数配置 29 | //module 模块 30 | //controller 控制器 31 | //method 函数名 32 | //$job =new JobObject('MyJob', 'module\controller', 'method', ['kcloze', time()]); 33 | public function start(JobObject $JobObject) 34 | { 35 | $this->init(); 36 | $urlInfo = explode('\\', $JobObject->jobClass); 37 | if (empty($urlInfo)) { 38 | Utils::catchError($this->logger, 'Yaf class must be config, please check'); 39 | die('Yaf class must be config, please check'); 40 | } 41 | $module = $urlInfo[0]; 42 | $controller = $urlInfo[1]; 43 | $action = $JobObject->jobMethod; 44 | $params = $JobObject->jobParams; 45 | try { 46 | if (empty(self::$application)) { 47 | defined('APPLICATION_PATH') ? '' : define('APPLICATION_PATH', SWOOLE_JOBS_ROOT_PATH); 48 | \Yaf\Loader::import(SWOOLE_JOBS_ROOT_PATH . '/application/init.php'); 49 | self::$application = new \Yaf\Application(SWOOLE_JOBS_ROOT_PATH . '/conf/application.ini', ini_get('yaf.environ')); 50 | } 51 | //此处params为固定参数名称,在yafAction里进行获取 52 | //public function methodAction($params){} 53 | $request = new \Yaf\Request\Simple('CLI', $module, $controller, $action, ['params'=>$params]); 54 | $response = self::$application->bootstrap()->getDispatcher()->returnResponse(true)->dispatch($request); 55 | unset($params); 56 | $this->logger->log('Action has been done, action content: ' . json_encode($JobObject)); 57 | } catch (\Throwable $e) { 58 | Utils::catchError($this->logger, $e); 59 | } catch (\Exception $e) { 60 | Utils::catchError($this->logger, $e); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Action/Yii1Action.php: -------------------------------------------------------------------------------- 1 | logger = Logs::getLogger(Config::getConfig()['logPath'] ?? '', Config::getConfig()['logSaveFileApp'] ?? '', Config::getConfig()['system'] ?? ''); 21 | } 22 | 23 | public function start(JobObject $JobObject) 24 | { 25 | $this->init(); 26 | 27 | try { 28 | if (defined('YII_ENV') && YII_ENV == 'development') { 29 | $name = 'console-dev.php'; 30 | } elseif (defined('YII_ENV') && YII_ENV == 'local') { 31 | $name = 'console-local.php'; 32 | } else { 33 | $name = 'console.php'; 34 | } 35 | $config = YCF_CONFIG_PATH . '/' . $name; 36 | require_once SWOOLE_JOBS_ROOT_PATH . '/../../framework/yii.php'; 37 | // Console Application 38 | $argv = ['yiic', $JobObject->jobClass, $JobObject->jobMethod]; 39 | $_SERVER['argv'] = array_merge($argv, $JobObject->jobParams); 40 | $application = new \CConsoleApplication($config); 41 | $application->processRequest(); 42 | $this->logger->log('Action has been done, action content: ' . json_encode($JobObject)); 43 | } catch (\Throwable $e) { 44 | Utils::catchError($this->logger, $e); 45 | } catch (\Exception $e) { 46 | Utils::catchError($this->logger, $e); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Action/YiiAction.php: -------------------------------------------------------------------------------- 1 | 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Kcloze\Jobs\Action; 11 | 12 | use Kcloze\Jobs\Config; 13 | use Kcloze\Jobs\JobObject; 14 | use Kcloze\Jobs\Logs; 15 | use Kcloze\Jobs\Utils; 16 | use yii\console\Application; 17 | 18 | class YiiAction extends BaseAction 19 | { 20 | private $logger=null; 21 | 22 | private static $application=null; 23 | 24 | public function init() 25 | { 26 | $this->logger = Logs::getLogger(Config::getConfig()['logPath'] ?? '', Config::getConfig()['logSaveFileApp'] ?? ''); 27 | } 28 | 29 | public function start(JobObject $JobObject) 30 | { 31 | $this->init(); 32 | $application = self::getApplication(); 33 | $route = strtolower($JobObject->jobClass) . '/' . $JobObject->jobMethod; 34 | $params = $JobObject->jobParams; 35 | try { 36 | $application->runAction($route, $params); 37 | \Yii::getLogger()->flush(true); 38 | $this->logger->log('Action has been done, action content: ' . json_encode($JobObject)); 39 | } catch (\Throwable $e) { 40 | Utils::catchError($this->logger, $e); 41 | } catch (\Exception $e) { 42 | Utils::catchError($this->logger, $e); 43 | } 44 | unset($application, $JobObject); 45 | } 46 | 47 | private static function getApplication() 48 | { 49 | if (self::$application === null) { 50 | $config = Config::getConfig()['framework']['config'] ?? []; 51 | self::$application = new Application($config); 52 | } 53 | 54 | return self::$application; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Api/Controller/Index.php: -------------------------------------------------------------------------------- 1 | pushSimple($data); 16 | } 17 | 18 | public function demo() 19 | { 20 | $data['topic'] = 'MyJob'; 21 | $data['jobClass'] = '\Kcloze\Jobs\Jobs\MyJob'; 22 | $data['jobMethod']= 'test2'; 23 | $data['jobParams']=['kcloze', time(), 'oop']; 24 | $data['jobExtras']=[]; 25 | $data['serializeFunc']='php'; 26 | 27 | $dataJob=json_encode($data); 28 | $pushJobs=new PushJobs(); 29 | return $pushJobs->pushSimple($dataJob); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Api/Services/PushJobs.php: -------------------------------------------------------------------------------- 1 | -3,'message'=>'sorry,jobData params is wrong.','content'=>$data]); 18 | } 19 | 20 | $data=\json_decode($jobData, true); 21 | $data['topic']=$data['topic']??''; 22 | $data['jobClass']=$data['jobClass']??''; 23 | $data['jobMethod']=$data['jobMethod']??''; 24 | $data['jobParams']=$data['jobParams']??''; 25 | $data['jobExtras']=$data['jobExtras']??''; 26 | $data['serializeFunc']=$data['serializeFunc']??'php'; 27 | //检查参数是否有误 28 | if (!$data['topic'] || !$data['jobClass'] || !$data['jobClass'] || !$data['jobParams']) { 29 | return \json_encode(['code'=>-2,'message'=>'no,jobData params is wrong.','content'=>$data]); 30 | } 31 | $pushJobs=new PushJobs(); 32 | $result=$pushJobs->push($data['topic'], $data['jobClass'], $data['jobMethod'], $data['jobParams'], $data['jobExtras'], $data['serializeFunc']); 33 | $data['uuid']=$result; 34 | if ($result) { 35 | return \json_encode(['code'=>100,'message'=>'ok,job has been pushed success.','content'=>$data]); 36 | } else { 37 | return \json_encode(['code'=>-1,'message'=>'sorry,job has been pushed fail.','content'=>$data]); 38 | } 39 | } 40 | public function push($topic, $jobClass, $jobMethod, $jobParams=[], $jobExtras=[], $serializeFunc='php') 41 | { 42 | $config = require SWOOLE_JOBS_ROOT_PATH . '/config.php'; 43 | $logger = Logs::getLogger($config['logPath'] ?? '', $config['logSaveFileApp'] ?? ''); 44 | $queue =Queue::getQueue($config['job']['queue'], $logger); 45 | $queue->setTopics($config['job']['topics']); 46 | 47 | // $jobExtras['delay'] =$delay; 48 | // $jobExtras['priority'] =BaseTopicQueue::HIGH_LEVEL_1; 49 | $job =new JobObject($topic, $jobClass, $jobMethod, $jobParams, $jobExtras); 50 | $result =$queue->push($topic, $job, 1, $serializeFunc); 51 | return $result; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Cache.php: -------------------------------------------------------------------------------- 1 | 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Kcloze\Jobs; 11 | 12 | use Exception; 13 | use Redis; 14 | 15 | class Cache 16 | { 17 | /** 18 | * @var Redis 19 | */ 20 | private $handler; 21 | private $config; 22 | 23 | /** 24 | * @param $config 25 | */ 26 | public function __construct($config) 27 | { 28 | $this->config = $config; 29 | $this->connect(); 30 | } 31 | 32 | /** 33 | * 调用redis. 34 | * 35 | * @param $method 36 | * @param $arguments 37 | * 38 | * @return mixed 39 | */ 40 | public function __call($method, $arguments) 41 | { 42 | if (!$this->handler) { 43 | $this->connect(); 44 | } 45 | 46 | return call_user_func_array([$this->handler, $method], $arguments); 47 | } 48 | 49 | public function get($key, $serialize = false) 50 | { 51 | if (!$this->handler) { 52 | $this->connect(); 53 | } 54 | if ($serialize === false) { 55 | isset($this->config['serialize']) && $serialize = $this->config['serialize']; 56 | } 57 | 58 | return $serialize ? unserialize($this->handler->get($key)) : $this->handler->get($key); 59 | } 60 | 61 | public function set($key, $value, $timeout = 0, $serialize = false) 62 | { 63 | if (!$this->handler) { 64 | $this->connect(); 65 | } 66 | if ($serialize === false) { 67 | isset($this->config['serialize']) && $serialize = $this->config['serialize']; 68 | } 69 | $value = $serialize ? serialize($value) : $value; 70 | if ($timeout > 0) { 71 | return $this->handler->set($key, $value, $timeout); 72 | } 73 | 74 | return $this->handler->set($key, $value); 75 | } 76 | 77 | public function hget($key, $hash, $serialize = false) 78 | { 79 | if (!$this->handler) { 80 | $this->connect(); 81 | } 82 | if ($serialize === false) { 83 | isset($this->config['serialize']) && $serialize = $this->config['serialize']; 84 | } 85 | 86 | return $serialize ? unserialize($this->handler->hget($key, $hash)) : $this->handler->hget($key, $hash); 87 | } 88 | 89 | public function hset($key, $hash, $value, $serialize = false) 90 | { 91 | if (!$this->handler) { 92 | $this->connect(); 93 | } 94 | if ($serialize === false) { 95 | isset($this->config['serialize']) && $serialize = $this->config['serialize']; 96 | } 97 | $value = $serialize ? serialize($value) : $value; 98 | 99 | return $this->handler->hset($key, $hash, $value); 100 | } 101 | 102 | /** 103 | * 创建handler. 104 | * 105 | * @throws Exception 106 | */ 107 | private function connect() 108 | { 109 | $this->handler = new Redis(); 110 | if (isset($this->config['keep-alive']) && $this->config['keep-alive']) { 111 | $fd = $this->handler->pconnect($this->config['host'], $this->config['port'], 60); 112 | } else { 113 | $fd = $this->handler->connect($this->config['host'], $this->config['port']); 114 | } 115 | if (isset($this->config['password'])) { 116 | $this->handler->auth($this->config['password']); 117 | } 118 | if (!$fd) { 119 | throw new Exception("Unable to connect to redis host: {$this->config['host']},port: {$this->config['port']}"); 120 | } 121 | //统一key前缀 122 | if (isset($this->config['preKey']) && !empty($this->config['preKey'])) { 123 | $this->handler->setOption(Redis::OPT_PREFIX, $this->config['preKey']); 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Command/AppCommand.php: -------------------------------------------------------------------------------- 1 | 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Kcloze\Jobs\Command; 11 | 12 | use Symfony\Component\Console\Input\InputArgument; 13 | use Kcloze\Jobs\Process; 14 | 15 | class AppCommand extends Command 16 | { 17 | protected static $defaultName = 'app'; 18 | 19 | public function __construct(array $config) 20 | { 21 | parent::__construct($config); 22 | } 23 | 24 | protected function configure() 25 | { 26 | $this->setDescription('manager swoole-jobs '); 27 | $this->addArgument('name', InputArgument::REQUIRED, 'Who do you want to start swoole-jobs?'); 28 | } 29 | 30 | 31 | protected function start() 32 | { 33 | //启动 34 | $process = new Process(); 35 | $process->start(); 36 | $this->output->writeln('swoole-jobs is starting.'); 37 | } 38 | 39 | protected function restart() 40 | { 41 | $this->logger->log('restarting...'); 42 | $this->stop(); 43 | sleep(3); 44 | $this->start(); 45 | } 46 | 47 | protected function stop() 48 | { 49 | $this->sendSignal(SIGUSR1); 50 | } 51 | 52 | protected function status() 53 | { 54 | $this->sendSignal(SIGUSR2); 55 | } 56 | 57 | protected function exit() 58 | { 59 | $this->sendSignal(SIGTERM); 60 | 61 | } 62 | 63 | protected function printHelpMessage() 64 | { 65 | $msg=<<<'EOF' 66 | NAME 67 | - manage swoole-jobs 68 | 69 | SYNOPSIS 70 | -php bin/swoole-jobs.php app [options] 71 | -Manage swoole-jobs daemons. 72 | 73 | WORKFLOWS 74 | 75 | -help [command] 76 | -Show this help, or workflow help for command. 77 | 78 | -restart 79 | -Stop, then start swoole-jobs master and workers. 80 | 81 | -start 82 | -Start swoole-jobs master and workers. 83 | 84 | -stop 85 | -Wait all running workers smooth exit, please check swoole-jobs status for a while. 86 | 87 | -exit 88 | -Kill all running workers and master PIDs. 89 | 90 | 91 | 92 | EOF; 93 | 94 | echo $msg; 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/Command/Command.php: -------------------------------------------------------------------------------- 1 | 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Kcloze\Jobs\Command; 11 | 12 | use Kcloze\Jobs\Config; 13 | use Kcloze\Jobs\Logs; 14 | use Kcloze\Jobs\Utils; 15 | use Symfony\Component\Console\Command\Command as SCommand; 16 | use Symfony\Component\Console\Input\InputInterface; 17 | use Symfony\Component\Console\Output\OutputInterface; 18 | 19 | abstract class Command extends SCommand 20 | { 21 | protected $input; 22 | protected $output; 23 | protected $config =[]; 24 | 25 | public function __construct(array $config) 26 | { 27 | parent::__construct(); 28 | Config::setConfig($config); 29 | $this->config = Config::getConfig(); 30 | $this->logger = new Logs($this->config['logPath'] ?? '', $this->config['logSaveFileApp'] ?? '', $this->config['system'] ?? ''); 31 | } 32 | 33 | protected function execute(InputInterface $input, OutputInterface $output) 34 | { 35 | $this->input =$input; 36 | $this->output=$output; 37 | $this->checkSwooleSetting(); 38 | $command=$this->input->getArgument('name'); 39 | 40 | switch ($command) { 41 | case 'start': 42 | $this->start(); 43 | break; 44 | case 'stop': 45 | $this->stop(); 46 | break; 47 | case 'restart': 48 | $this->restart(); 49 | break; 50 | case 'status': 51 | $this->status(); 52 | break; 53 | case 'exit': 54 | $this->exit(); 55 | break; 56 | case 'help': 57 | $this->printHelpMessage(); 58 | break; 59 | 60 | default: 61 | $this->printHelpMessage(); 62 | break; 63 | } 64 | return 0; 65 | } 66 | 67 | abstract protected function start(); 68 | 69 | abstract protected function restart(); 70 | 71 | abstract protected function status(); 72 | 73 | abstract protected function stop(); 74 | 75 | abstract protected function exit(); 76 | 77 | /** 78 | * 给主进程发送信号: 79 | * SIGUSR1 自定义信号,让子进程平滑退出 80 | * SIGUSR2 自定义信号2,显示进程状态 81 | * SIGTERM 程序终止,让子进程强制退出. 82 | * 83 | * @param [type] $signal 84 | */ 85 | protected function sendSignal($signal=SIGUSR1) 86 | { 87 | $this->logger->log($signal . (SIGUSR1 == $signal) ? ' smooth to exit...' : ' force to exit...'); 88 | 89 | if (isset($this->config['pidPath']) && !empty($this->config['pidPath'])) { 90 | $this->config['pidPath']=$this->config['pidPath'] . '/' . Utils::getHostName(); 91 | $masterPidFile =$this->config['pidPath'] . '/master.pid'; 92 | $pidStatusFile =$this->config['pidPath'] . '/status.info'; 93 | } else { 94 | echo 'config pidPath must be set!' . PHP_EOL; 95 | 96 | return; 97 | } 98 | 99 | if (file_exists($masterPidFile)) { 100 | $pid =file_get_contents($masterPidFile); 101 | if (!$pid) { 102 | echo 'swoole-jobs pid is null' . PHP_EOL; 103 | 104 | return; 105 | } 106 | 107 | if ($pid && !@\Swoole\Process::kill($pid, 0)) { 108 | echo 'service is not running' . PHP_EOL; 109 | 110 | return; 111 | } 112 | if (@\Swoole\Process::kill($pid, $signal)) { 113 | $this->logger->log('[master pid: ' . $pid . '] has been received signal' . $signal); 114 | sleep(1); 115 | //如果是SIGUSR2信号,显示swoole-jobs状态信息 116 | if (SIGUSR2 == $signal) { 117 | $statusStr=@file_get_contents($pidStatusFile); 118 | 119 | echo $statusStr ? $statusStr : 'sorry,show status fail.'; 120 | @unlink($pidStatusFile); 121 | 122 | return; 123 | } elseif (SIGTERM == $signal) { 124 | //尝试5次发送信号 125 | $i=0; 126 | do { 127 | ++$i; 128 | $this->logger->log('[master pid: ' . $pid . '] has been received signal' . $signal . ' times: ' . $i); 129 | if (!@\Swoole\Process::kill($pid, 0)) { 130 | echo 'swoole-jobs kill successful, status is stopped.' . PHP_EOL; 131 | 132 | return; 133 | } 134 | @\Swoole\Process::kill($pid, $signal); 135 | 136 | sleep(3); 137 | } while ($i <= 5); 138 | 139 | echo 'swoole-jobs kill failed.' . PHP_EOL; 140 | } 141 | echo 'swoole-jobs stop success.' . PHP_EOL; 142 | } 143 | $this->logger->log('[master pid: ' . $pid . '] has been received signal fail'); 144 | 145 | return; 146 | } 147 | echo 'service is not running' . PHP_EOL; 148 | } 149 | 150 | protected function sendSignalHttpServer($signal=SIGTERM) 151 | { 152 | if (isset($this->config['httpServer']) && isset($this->config['httpServer']['settings']['pid_file'])) { 153 | $httpServerPid =null; 154 | file_exists($this->config['httpServer']['settings']['pid_file']) && $httpServerPid =file_get_contents($this->config['httpServer']['settings']['pid_file']); 155 | if (!$httpServerPid) { 156 | echo 'http server pid is null, maybe http server is not running!' . PHP_EOL; 157 | 158 | return; 159 | } 160 | //尝试5次发送信号 161 | $i=0; 162 | do { 163 | ++$i; 164 | $this->logger->log('[httpServerPid : ' . $httpServerPid . '] has been received signal' . $signal . ' times: ' . $i); 165 | if (!@\Swoole\Process::kill($httpServerPid, 0)) { 166 | echo 'http server status is stopped' . PHP_EOL; 167 | 168 | return; 169 | } 170 | @\Swoole\Process::kill($httpServerPid, $signal); 171 | 172 | sleep(1); 173 | } while ($i <= 5); 174 | echo 'swoole-jobs kill failed.' . PHP_EOL; 175 | } else { 176 | echo 'configs with http server not settting' . PHP_EOL; 177 | 178 | return; 179 | } 180 | } 181 | 182 | private function checkSwooleSetting() 183 | { 184 | if (version_compare(swoole_version(), '4.4.4', '<=') && version_compare(swoole_version(), '4.0.0', '>=') && 'Off' !== ini_get('swoole.enable_coroutine')) { 185 | $this->output->writeln('4.0.0 <= swoole version <=4.4.4,you have to disable coroutine in php.ini, or you can update swoole version >=4.4.4'); 186 | $this->output->writeln('details jump to: https://github.com/swoole/swoole-src/issues/2716'); 187 | exit; 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/Command/HttpCommand.php: -------------------------------------------------------------------------------- 1 | 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Kcloze\Jobs\Command; 11 | 12 | use Kcloze\Jobs\HttpServer; 13 | use Symfony\Component\Console\Input\InputArgument; 14 | 15 | class HttpCommand extends Command 16 | { 17 | protected static $defaultName = 'http'; 18 | 19 | public function __construct(array $config) 20 | { 21 | parent::__construct($config); 22 | } 23 | 24 | protected function configure() 25 | { 26 | $this->setDescription('manager swoole-jobs http api '); 27 | $this->addArgument('name', InputArgument::REQUIRED, 'Who do you want to start swoole-jobs http api?'); 28 | } 29 | 30 | protected function start() 31 | { 32 | //启动 33 | if (isset($this->config['httpServer'])) { 34 | $this->output->writeln('swoole-jobs http server is starting.'); 35 | HttpServer::getInstance($this->config); 36 | } else { 37 | $this->output->writeln('sorrry,swoole-jobs http server config is not setting!'); 38 | } 39 | } 40 | 41 | protected function restart() 42 | { 43 | $this->logger->log('api server restarting...'); 44 | $this->stop(); 45 | sleep(3); 46 | $this->start(); 47 | } 48 | 49 | protected function stop() 50 | { 51 | $this->sendSignalHttpServer(SIGTERM); 52 | } 53 | 54 | protected function status() 55 | { 56 | $this->output->writeln('there is no status command.'); 57 | } 58 | 59 | protected function exit() 60 | { 61 | $this->sendSignalHttpServer(SIGTERM); 62 | } 63 | 64 | protected function printHelpMessage() 65 | { 66 | $msg=<<<'EOF' 67 | NAME 68 | - swoole-jobs http api 69 | 70 | SYNOPSIS 71 | -php ./bin/swoole-jobs.php http [options] 72 | -Manage swoole-jobs http api. 73 | 74 | WORKFLOWS 75 | 76 | -help [command] 77 | -Show this help, or workflow help for command. 78 | 79 | -http start 80 | -Start swoole http server for apis. 81 | 82 | -http stop 83 | -Stop swoole http server for api. 84 | 85 | -http exit 86 | -Stop swoole http server for api. 87 | 88 | 89 | EOF; 90 | 91 | echo $msg; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Config.php: -------------------------------------------------------------------------------- 1 | 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Kcloze\Jobs; 11 | 12 | class Config 13 | { 14 | private static $config=[]; 15 | 16 | public static function setConfig($config) 17 | { 18 | self::$config=$config; 19 | } 20 | 21 | public static function getConfig() 22 | { 23 | return self::$config; 24 | } 25 | 26 | /** 27 | * 从配置数组中获取相关值 28 | * $config=[ 29 | * ['name'=>'MyJob', 'workerMinNum'=>1, 'workerMaxNum'=>3, 'queueMaxNum'=>10000, 'queueMaxNumForProcess' => 100, 'autoAckBeforeJobStart'=>false], 30 | * ['name'=> 'MyJob2', 'workerMinNum'=>1, 'workerMaxNum'=>3], 31 | * ['name'=> 'MyJob3', 'workerMinNum'=>1, 'workerMaxNum'=>1], 32 | * ['name'=> 'DefaultClassMethod.test1', 'workerMinNum'=>1, 'workerMaxNum'=>2, 'defaultJobClass'=>'DefaultClassMethod', 'defaultJobMethod'=>'test1'] 33 | * ]. 34 | * 35 | * @param mixed $config 36 | * @param mixed $topic 37 | * @param mixed $name 38 | * */ 39 | public static function getTopicConfig($config, $topic, $name) 40 | { 41 | $key=array_search($topic, array_column($config, 'name'), true); 42 | 43 | return $config[$key][$name] ?? true; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/HttpServer.php: -------------------------------------------------------------------------------- 1 | 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Kcloze\Jobs; 11 | 12 | class HttpServer 13 | { 14 | public static $instance; 15 | 16 | public $http; 17 | public static $get; 18 | public static $post; 19 | public static $header; 20 | public static $server; 21 | public $response = null; 22 | private $application; 23 | 24 | public function __construct($config=[]) 25 | { 26 | $host =$config['httpServer']['host'] ?? '0.0.0.0'; 27 | $port =$config['httpServer']['port'] ?? 9501; 28 | $http = new \Swoole\Http\Server($host, $port); 29 | 30 | $http->set( 31 | $config['httpServer']['settings'] 32 | ); 33 | 34 | $http->on('WorkerStart', [$this, 'onWorkerStart']); 35 | 36 | $http->on('request', function ($request, $response) { 37 | date_default_timezone_set('Asia/Shanghai'); 38 | 39 | //define('SWOOLE_JOBS_ROOT_PATH', __DIR__ . '/..'); 40 | //捕获异常 41 | register_shutdown_function([$this, 'handleFatal']); 42 | //请求过滤 43 | if ('/favicon.ico' == $request->server['path_info'] || '/favicon.ico' == $request->server['request_uri']) { 44 | return $response->end(); 45 | } 46 | $this->response = $response; 47 | if (isset($request->server)) { 48 | HttpServer::$server = $request->server; 49 | foreach ($request->server as $key => $value) { 50 | $_SERVER[strtoupper($key)] = $value; 51 | } 52 | } 53 | if (isset($request->header)) { 54 | HttpServer::$header = $request->header; 55 | } 56 | if (isset($request->get)) { 57 | HttpServer::$get = $request->get; 58 | foreach ($request->get as $key => $value) { 59 | $_GET[$key] = $value; 60 | } 61 | } 62 | if (isset($request->post)) { 63 | HttpServer::$post = $request->post; 64 | foreach ($request->post as $key => $value) { 65 | $_POST[$key] = $value; 66 | } 67 | } 68 | ob_start(); 69 | //实例化slim对象 70 | try { 71 | $router = new \Kcloze\Jobs\Router(); 72 | $router->run(); 73 | } catch (\Exception $e) { 74 | var_dump($e); 75 | } 76 | $result = ob_get_contents(); 77 | ob_end_clean(); 78 | $response->end($result); 79 | unset($result, $router,$_GET,$_POST,$_SERVER); 80 | }); 81 | 82 | $http->start(); 83 | } 84 | 85 | /** 86 | * Fatal Error的捕获. 87 | */ 88 | public function handleFatal() 89 | { 90 | $error = error_get_last(); 91 | if (!isset($error['type'])) { 92 | return; 93 | } 94 | 95 | switch ($error['type']) { 96 | case E_ERROR: 97 | case E_PARSE: 98 | case E_DEPRECATED: 99 | case E_CORE_ERROR: 100 | case E_COMPILE_ERROR: 101 | break; 102 | default: 103 | return; 104 | } 105 | $message = $error['message']; 106 | $file = $error['file']; 107 | $line = $error['line']; 108 | $log = "\n异常提示:$message ($file:$line)\nStack trace:\n"; 109 | $trace = debug_backtrace(1); 110 | 111 | foreach ($trace as $i => $t) { 112 | if (!isset($t['file'])) { 113 | $t['file'] = 'unknown'; 114 | } 115 | if (!isset($t['line'])) { 116 | $t['line'] = 0; 117 | } 118 | if (!isset($t['function'])) { 119 | $t['function'] = 'unknown'; 120 | } 121 | $log .= "#$i {$t['file']}({$t['line']}): "; 122 | if (isset($t['object']) && is_object($t['object'])) { 123 | $log .= get_class($t['object']) . '->'; 124 | } 125 | $log .= "{$t['function']}()\n"; 126 | } 127 | if (isset($_SERVER['REQUEST_URI'])) { 128 | $log .= '[QUERY] ' . $_SERVER['REQUEST_URI']; 129 | } 130 | 131 | if ($this->response) { 132 | $this->response->status(500); 133 | $this->response->end($log); 134 | } 135 | 136 | unset($this->response); 137 | } 138 | 139 | public function onWorkerStart() 140 | { 141 | require __DIR__ . '/../vendor/autoload.php'; 142 | // session_start(); 143 | } 144 | 145 | public static function getInstance($config) 146 | { 147 | if (!self::$instance) { 148 | self::$instance = new self($config); 149 | } 150 | 151 | return self::$instance; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/JobObject.php: -------------------------------------------------------------------------------- 1 | 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Kcloze\Jobs; 11 | 12 | class JobObject 13 | { 14 | public $uuid =''; //job uuid 15 | public $topic =''; //job 队列名 16 | public $jobClass =''; //job 执行类 17 | public $jobMethod =''; //job 执行方法 18 | public $jobParams =[]; //job参数 19 | public $jobExtras =[]; //附件信息,delay/expiration/priority等 20 | 21 | public function __construct(string $topic, string $jobClass, string $jobMethod, array $jobParams=[], array $jobExtras=[], $uuid='') 22 | { 23 | $this->uuid =empty($uuid) ? uniqid($topic) . '.' . Utils::getMillisecond() : $uuid; 24 | $this->topic =$topic; 25 | $this->jobClass =$jobClass; 26 | $this->jobMethod =$jobMethod; 27 | $this->jobParams =$jobParams; 28 | $this->jobExtras =$jobExtras; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Jobs.php: -------------------------------------------------------------------------------- 1 | 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Kcloze\Jobs; 11 | 12 | use Kcloze\Jobs\Queue\Queue; 13 | 14 | class Jobs 15 | { 16 | const MinTimeJob =0.0001; //job最少执行时间,少于这个时间不正常,用于worker安全进程退出依据 17 | const MaxTimeJob =15; //job最大执行时间,大于这个时间不正常,用于worker安全进程退出依据 18 | 19 | public $logger = null; 20 | public $queue = null; 21 | public $sleep = 2; //单个topic如果没有任务,该进程暂停秒数,不能低于1秒,数值太小无用进程会频繁拉起 22 | public $config = []; 23 | 24 | public $popNum = 0; // 用来记录job执行次数,操过次数退出循环 25 | public $maxPopNum = 500; // 子进程启动后每个循环最多取多少个job,该参数已经删除 26 | private $pidInfoFile = ''; // 主进程pid信息文件路径 27 | 28 | public function __construct($pidInfoFile) 29 | { 30 | $this->config = Config::getConfig(); //读取配置文件 31 | $this->pidInfoFile = $pidInfoFile; 32 | $this->sleep = $this->config['sleep'] ?? $this->sleep; 33 | $this->maxPopNum = $this->config['maxPopNum'] ?? $this->maxPopNum; 34 | $this->logger = Logs::getLogger($this->config['logPath'] ?? '', $this->config['logSaveFileApp'] ?? '', $this->config['system'] ?? ''); 35 | } 36 | 37 | public function run($topic='') 38 | { 39 | if ($topic) { 40 | $this->queue = Queue::getQueue($this->config['job']['queue'], $this->logger); 41 | if (empty($this->queue)) { 42 | sleep($this->sleep); 43 | 44 | return; 45 | } 46 | $this->queue->setTopics($this->config['job']['topics'] ?? []); 47 | 48 | $len = $this->queue->len($topic); 49 | //$this->logger->log($topic . ' pop len: ' . $len, 'info'); 50 | if ($len > 0) { 51 | /* 52 | * 循环拿出队列消息 53 | * 每次最多取maxPopNum个任务执行 54 | */ 55 | $slpTimes=0; //空消息达到一定次数,说明队列确实没有消息 56 | do { 57 | ++$this->popNum; 58 | 59 | //主进程状态不是running状态,退出循环 60 | if (Process::STATUS_RUNNING != $this->getMasterData('status')) { 61 | break; 62 | } 63 | 64 | $this->queue && $data = $this->queue->pop($topic); 65 | 66 | if (empty($data)) { 67 | ++$slpTimes; 68 | if ($slpTimes > 10) { 69 | //空消息达到一定次数,说明队列确实没有消息 70 | break; 71 | } 72 | //暂停1毫秒 73 | usleep(1000); 74 | continue; 75 | } 76 | 77 | $this->logger->log('pop data: ' . json_encode($data), 'info'); 78 | $autoAckBeforeJobStart=Config::getTopicConfig($this->config['job']['topics'], $topic, 'autoAckBeforeJobStart') ?? true; 79 | if (true === $autoAckBeforeJobStart) { 80 | $this->queue->ack(); 81 | } 82 | if (!empty($data) && (\is_object($data) || \is_array($data))) { 83 | $beginTime=microtime(true); 84 | // 根据自己的业务需求改写此方法 85 | $jobObject = $this->loadObject($data); 86 | if ($jobObject instanceof JobObject) { 87 | $jobObject = $this->formatJobObjectByTopicConfig($jobObject, $topic, $data); 88 | } 89 | $baseAction = $this->loadFrameworkAction(); 90 | $baseAction->start($jobObject); 91 | $endTime =microtime(true); 92 | $execTime=$endTime - $beginTime; 93 | $this->logger->log('pid: ' . getmypid() . ', job id: ' . $jobObject->uuid . ' done, spend time: ' . $execTime, 'info'); 94 | //确认消息安全消费完成 95 | if (true !== $autoAckBeforeJobStart) { 96 | $this->queue->ack(); 97 | } 98 | //黑科技:实践中发现有可能进不到业务代码,造成消息丢失,job执行太快或者太慢(业务出现异常),worker进程都安全退出 99 | $minTimeJob=$this->config['job']['profile']['minTime'] ?? self::MinTimeJob; 100 | $maxTimeJob=$this->config['job']['profile']['maxTime'] ?? self::MaxTimeJob; 101 | if ($execTime < $minTimeJob || $execTime > $maxTimeJob) { 102 | //$this->queue->push($topic, $jobObject); 103 | $msgJobError=($execTime < $minTimeJob) ? 'too fast' : 'too slow'; 104 | $this->logger->log('job execute ' . $msgJobError . ', uuid: ' . $jobObject->uuid . ', execTime:' . $execTime, 'error', 'error'); 105 | //进程安全退出 106 | exit; 107 | } 108 | } else { 109 | $this->logger->log('pop error data: ' . print_r($data, true), 'error', 'error'); 110 | } 111 | //防止内存泄漏,每次执行一个job就退出[极端情况才需要开启] 112 | if (isset($this->config['eachJobExit']) && true == $this->config['eachJobExit']) { 113 | $this->logger->log('Each Job Exit, job id: ' . $jobObject->uuid . PHP_EOL); 114 | exit; 115 | } 116 | // if ($this->queue->len($topic) <= 0) { 117 | // break; 118 | // } 119 | unset($jobObject, $baseAction); 120 | } while ($this->popNum <= $this->maxPopNum); 121 | } else { 122 | sleep($this->sleep); 123 | } 124 | // $this->queue->close(); 125 | // Queue::$_instance=null; 126 | } else { 127 | $this->logger->log('All topic no work to do!', 'info'); 128 | } 129 | } 130 | 131 | /** 132 | * 获取topic配置对象,格式化JobObject. 133 | * 134 | * @param JobObject $jobObject 135 | * @param string $topic 136 | * @param mixed $data 137 | * 138 | * @return JobObject 139 | */ 140 | public function formatJobObjectByTopicConfig(JobObject $jobObject, $topic, $data) 141 | { 142 | $topicConfigObject = new TopicConfigObject(); 143 | if ('' == $topic) { 144 | return $jobObject; 145 | } 146 | if ('' === $jobObject->topic) { 147 | $jobObject->topic = $topic; 148 | } 149 | //如果消息体对象的callback class或method为空,则尝试读取配置的默认class和method 150 | if ('' == $jobObject->jobClass || '' == $jobObject->jobMethod) { 151 | $topicConfig = $this->getConfigByTopic($topic); 152 | if ($topicConfig != []) { 153 | $topicConfigObject->initAttributes($topicConfig); 154 | if ('' == $jobObject->jobClass) { 155 | $jobObject->jobClass = $topicConfigObject->getDefaultJobClass(); 156 | } 157 | if ('' == $jobObject->jobMethod) { 158 | $jobObject->jobMethod = $topicConfigObject->getDefaultJobMethod(); 159 | } 160 | if ($jobObject->jobParams == []) { 161 | $jobObject->jobParams = $data; 162 | } 163 | } 164 | } 165 | 166 | return $jobObject; 167 | } 168 | 169 | /** 170 | * 获取对应topic的配置数组. 171 | * 172 | * @param string $topic 173 | * 174 | * @return array 175 | */ 176 | public function getConfigByTopic($topic) 177 | { 178 | $topicsConfig = $this->config['job']['topics'] ?? []; 179 | $topicConfig = array_filter($topicsConfig, function ($config) use ($topic) { 180 | return $config['name'] == $topic; 181 | }); 182 | 183 | return $topicConfig != [] ? reset($topicConfig) : []; 184 | } 185 | 186 | //根据配置装入不同的框架 187 | private function loadFrameworkAction() 188 | { 189 | $classFramework=$this->config['framework']['class'] ?? '\Kcloze\Jobs\Action\SwooleJobsAction'; 190 | try { 191 | $action = new $classFramework(); 192 | } catch (\Throwable $e) { 193 | Utils::catchError($this->logger, $e); 194 | } catch (\Exception $e) { 195 | Utils::catchError($this->logger, $e); 196 | } 197 | 198 | return $action; 199 | } 200 | 201 | //实例化job对象 202 | private function loadObject($data) 203 | { 204 | if (\is_object($data)) { 205 | return new JobObject($data->topic ?? '', $data->jobClass ?? '', $data->jobMethod ?? '', $data->jobParams ?? [], $data->jobExtras ?? [], $data->uuid ?? ''); 206 | } elseif (\is_array($data)) { 207 | return new JobObject($data['topic'] ?? '', $data['jobClass'] ?? '', $data['jobMethod'] ?? '', $data['jobParams'] ?? [], $data['jobExtras'] ?? [], $data['uuid'] ?? ''); 208 | } 209 | 210 | return false; 211 | } 212 | 213 | private function getMasterData($key='') 214 | { 215 | $data=unserialize(file_get_contents($this->pidInfoFile)); 216 | if ($key) { 217 | return $data[$key] ?? null; 218 | } 219 | 220 | return $data; 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/Jobs/DefaultClassMethod.php: -------------------------------------------------------------------------------- 1 | ',__FUNCTION__,'(' . var_export($args, true) . ')',PHP_EOL; 10 | } 11 | 12 | public function test2(...$args) 13 | { 14 | echo __CLASS__,'->',__FUNCTION__,'(' . var_export($args, true) . ')',PHP_EOL; 15 | } 16 | 17 | public static function test3(...$args) 18 | { 19 | echo __CLASS__,'::',__FUNCTION__,'(' . var_export($args, true) . ')',PHP_EOL; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Jobs/MyJob.php: -------------------------------------------------------------------------------- 1 | 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Kcloze\Jobs\Jobs; 11 | 12 | class MyJob 13 | { 14 | public static function test1($a, $b) 15 | { 16 | sleep(1); 17 | // $client = new \GuzzleHttp\Client(); 18 | // $res = $client->request('GET', 'https://www.oschina.net/', ['timeout' => 3]); 19 | // echo $res->getStatusCode() . ' test1| title: ' . $a . ' time: ' . $b . PHP_EOL; 20 | die('oh,my gad!').PHP_EOL;//模拟job异常退出,如果设置autoAckBeforeJobStart为false,则消息出队列之后会回到队列,消息不丢失 21 | echo ' test1| title: ' . $a . ' time: ' . $b . PHP_EOL; 22 | } 23 | 24 | public function test2($a, $b, $c) 25 | { 26 | sleep(3); 27 | // $client = new \GuzzleHttp\Client(); 28 | // $res = $client->request('GET', 'https://www.oschina.net/', ['timeout' => 3]); 29 | // echo $res->getStatusCode() . ' test2| title: ' . $a . ' time: ' . $b . ' ' . print_r($c, true) . PHP_EOL; 30 | 31 | echo ' test2| title: ' . $a . ' time: ' . $b . ' ' . print_r($c, true) . PHP_EOL; 32 | } 33 | 34 | public function testError($a, $b) 35 | { 36 | //随机故意构造错误,验证子进程推出情况 37 | $i = mt_rand(0, 5); 38 | if (3 == $i) { 39 | echo '出错误了!!!' . PHP_EOL; 40 | try { 41 | $this->methodNoFind(); 42 | new Abc(); 43 | } catch (\Exception $e) { 44 | var_dump($e->getMessage()); 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Jobs/MyJob2.php: -------------------------------------------------------------------------------- 1 | 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Kcloze\Jobs\Jobs; 11 | 12 | class MyJob2 13 | { 14 | public static function test1($a, $b) 15 | { 16 | sleep(1); 17 | // $client = new \GuzzleHttp\Client(); 18 | // $res = $client->request('GET', 'https://www.baidu.com', ['timeout' => 3]); 19 | // echo $res->getStatusCode() . ' test1| title: ' . $a . ' time: ' . $b . PHP_EOL; 20 | 21 | echo ' test1| title: ' . $a . ' time: ' . $b . PHP_EOL; 22 | } 23 | 24 | public function test2($a, $b, $c) 25 | { 26 | sleep(2); 27 | // $client = new \GuzzleHttp\Client(); 28 | // $res = $client->request('GET', 'https://www.baidu.com', ['timeout' => 3]); 29 | // echo $res->getStatusCode() . ' test2| title: ' . $a . ' time: ' . $b . ' ' . print_r($c, true) . PHP_EOL; 30 | echo ' test2| title: ' . $a . ' time: ' . $b . ' ' . print_r($c, true) . PHP_EOL; 31 | } 32 | 33 | public function testError($a, $b) 34 | { 35 | //随机故意构造错误,验证子进程推出情况 36 | $i = mt_rand(0, 5); 37 | if (3 == $i) { 38 | echo '出错误了!!!' . PHP_EOL; 39 | try { 40 | $this->methodNoFind(); 41 | new Abc(); 42 | } catch (\Exception $e) { 43 | var_dump($e->getMessage()); 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Logs.php: -------------------------------------------------------------------------------- 1 | 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Kcloze\Jobs; 11 | 12 | class Logs 13 | { 14 | const LEVEL_TRACE = 'trace'; 15 | const LEVEL_WARNING = 'warning'; 16 | const LEVEL_ERROR = 'error'; 17 | const LEVEL_INFO = 'info'; 18 | const LEVEL_PROFILE = 'profile'; 19 | const MAX_LOGS = 10000; 20 | 21 | public $rotateByCopy = false; 22 | public $maxLogFiles = 5; 23 | public $maxFileSize = 1000; // in MB 24 | 25 | //系统日志标识 26 | private $logSystem = 'swoole-jobs'; 27 | 28 | private $logPath = ''; 29 | //单个类型log 30 | private $logs = []; 31 | private $logCount = 0; 32 | //默认log文件存储名 33 | private $logSaveFileApp = 'application.log'; 34 | 35 | private static $instance=null; 36 | 37 | public function __construct($logPath, $logSaveFileApp='', $logSystem = '') 38 | { 39 | if (empty($logPath)) { 40 | die('config logPath must be set!' . PHP_EOL); 41 | } 42 | Utils::mkdir($logPath); 43 | $this->logPath = $logPath; 44 | if ($logSaveFileApp) { 45 | $this->logSaveFileApp = $logSaveFileApp; 46 | } 47 | 48 | $logSystem && $this->logSystem = $logSystem; 49 | } 50 | 51 | /** 52 | * 获取日志实例. 53 | * 54 | * @$logPath 55 | * 56 | * @param mixed $logPath 57 | * @param mixed $logSaveFileApp 58 | * @param mixed $logSystem 59 | */ 60 | public static function getLogger($logPath='', $logSaveFileApp='', $logSystem = '') 61 | { 62 | if (isset(self::$instance) && null !== self::$instance) { 63 | return self::$instance; 64 | } 65 | self::$instance=new self($logPath, $logSaveFileApp, $logSystem); 66 | 67 | return self::$instance; 68 | } 69 | 70 | /** 71 | * 格式化日志信息. 72 | * 73 | * @param mixed $message 74 | * @param mixed $level 75 | * @param mixed $category 76 | * @param mixed $time 77 | */ 78 | public function formatLogMessage($message, $level, $category, $time) 79 | { 80 | $pid = getmypid(); 81 | 82 | return @date('Y/m/d H:i:s', $time) . " YCFLOG [$this->logSystem] [$level] [$category] [PID$pid] \n $message \n"; 83 | } 84 | 85 | /** 86 | * 日志分类处理. 87 | * 88 | * @param mixed $message 89 | * @param mixed $level 90 | * @param mixed $category 91 | * @param mixed $flush 92 | */ 93 | public function log($message, $level = 'info', $category = '', $flush = true) 94 | { 95 | if (empty($category)) { 96 | $category=$this->logSaveFileApp; 97 | } 98 | $this->logs[$category][] = [$message, $level, $category, microtime(true)]; 99 | ++$this->logCount; 100 | if ($this->logCount >= self::MAX_LOGS || true == $flush) { 101 | $this->flush($category); 102 | } 103 | } 104 | 105 | /** 106 | * 日志分类处理. 107 | */ 108 | public function processLogs() 109 | { 110 | $logsAll=[]; 111 | foreach ((array) $this->logs as $key => $logs) { 112 | $logsAll[$key] = ''; 113 | foreach ((array) $logs as $log) { 114 | $logsAll[$key] .= $this->formatLogMessage($log[0], $log[1], $log[2], $log[3]); 115 | } 116 | } 117 | 118 | return $logsAll; 119 | } 120 | 121 | /** 122 | * 写日志到文件. 123 | */ 124 | public function flush() 125 | { 126 | if ($this->logCount <= 0) { 127 | return false; 128 | } 129 | $logsAll = $this->processLogs(); 130 | $this->write($logsAll); 131 | $this->logs = []; 132 | $this->logCount = 0; 133 | } 134 | 135 | /** 136 | * [write 根据日志类型写到不同的日志文件]. 137 | * 138 | * @param $logsAll 139 | * 140 | * @throws \Exception 141 | */ 142 | public function write($logsAll) 143 | { 144 | if (empty($logsAll)) { 145 | return; 146 | } 147 | //$this->logPath = ROOT_PATH . 'src/runtime/'; 148 | if (!is_dir($this->logPath)) { 149 | self::mkdir($this->logPath, [], true); 150 | } 151 | foreach ($logsAll as $key => $value) { 152 | if (empty($key)) { 153 | continue; 154 | } 155 | //日志分类文件夹 156 | $keyCat = strtr($key, ['.log'=>'']); 157 | //日志文件名 158 | $key = strtr($key, ['.log'=>'']) . '-' . date('Ymd', time()) . '.log'; 159 | if (!is_dir($this->logPath . '/' . $keyCat)) { 160 | self::mkdir($this->logPath . '/' . $keyCat, [], true); 161 | } 162 | 163 | $fileName = $this->logPath . '/' . $keyCat . '/' . $key; 164 | 165 | if (false === ($fp = @fopen($fileName, 'a'))) { 166 | throw new \Exception("Unable to append to log file: {$fileName}"); 167 | } 168 | @flock($fp, LOCK_EX); 169 | 170 | if (@filesize($fileName) > $this->maxFileSize * 1024 * 1024) { 171 | $this->rotateFiles($fileName); 172 | } 173 | @fwrite($fp, $value); 174 | @flock($fp, LOCK_UN); 175 | @fclose($fp); 176 | } 177 | } 178 | 179 | /** 180 | * Rotates log files. 181 | * 182 | * @param mixed $file 183 | */ 184 | protected function rotateFiles($file) 185 | { 186 | for ($i = $this->maxLogFiles; $i >= 0; --$i) { 187 | // $i == 0 is the original log file 188 | $rotateFile = $file . (0 === $i ? '' : '.' . $i); 189 | //var_dump($rotateFile); 190 | if (is_file($rotateFile)) { 191 | // suppress errors because it's possible multiple processes enter into this section 192 | if ($i === $this->maxLogFiles) { 193 | @unlink($rotateFile); 194 | } else { 195 | if ($this->rotateByCopy) { 196 | @copy($rotateFile, $file . '.' . ($i + 1)); 197 | if ($fp = @fopen($rotateFile, 'a')) { 198 | @ftruncate($fp, 0); 199 | @fclose($fp); 200 | } 201 | } else { 202 | @rename($rotateFile, $file . '.' . ($i + 1)); 203 | } 204 | } 205 | } 206 | } 207 | } 208 | 209 | /** 210 | * Shared environment safe version of mkdir. Supports recursive creation. 211 | * For avoidance of umask side-effects chmod is used. 212 | * 213 | * @param string $dst path to be created 214 | * @param array $options newDirMode element used, must contain access bitmask 215 | * @param bool $recursive whether to create directory structure recursive if parent dirs do not exist 216 | * 217 | * @return bool result of mkdir 218 | * 219 | * @see mkdir 220 | */ 221 | private static function mkdir($dst, array $options, $recursive) 222 | { 223 | $prevDir = dirname($dst); 224 | if ($recursive && !is_dir($dst) && !is_dir($prevDir)) { 225 | self::mkdir(dirname($dst), $options, true); 226 | } 227 | $mode = isset($options['newDirMode']) ? $options['newDirMode'] : 0777; 228 | $res = mkdir($dst, $mode); 229 | @chmod($dst, $mode); 230 | 231 | return $res; 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /src/Message/BaseMessage.php: -------------------------------------------------------------------------------- 1 | 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Kcloze\Jobs\Message; 11 | 12 | abstract class BaseMessage 13 | { 14 | public function init() 15 | { 16 | } 17 | 18 | public function send(string $message) 19 | { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Message/DingMessage.php: -------------------------------------------------------------------------------- 1 | 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Kcloze\Jobs\Message; 11 | 12 | use Kcloze\Jobs\Config; 13 | use Kcloze\Jobs\Logs; 14 | use Kcloze\Jobs\Utils; 15 | 16 | class DingMessage 17 | { 18 | private $apiUrl='https://oapi.dingtalk.com/robot/send'; 19 | 20 | public function init() 21 | { 22 | $this->logger = Logs::getLogger(Config::getConfig()['logPath'] ?? '', Config::getConfig()['logSaveFileApp'] ?? '', Config::getConfig()['system'] ?? ''); 23 | } 24 | 25 | public function send(string $content, string $token) 26 | { 27 | $this->init(); 28 | if (!$token || !$content) { 29 | return false; 30 | } 31 | try { 32 | $message = ['msgtype' => 'text', 'text' => ['content' => $content], 'at' => ['atMobiles' => [], 'isAtAll' => false]]; 33 | $apiUrl = $this->apiUrl . '?access_token=' . $token; 34 | $client = new \GuzzleHttp\Client(); 35 | $res = $client->request('POST', $apiUrl, ['json' => $message, 'timeout' => 5]); 36 | $httpCode =$res->getStatusCode(); 37 | $body =$res->getBody(); 38 | } catch (\Throwable $e) { 39 | Utils::catchError($this->logger, $e); 40 | } catch (\Exception $e) { 41 | Utils::catchError($this->logger, $e); 42 | } 43 | 44 | $this->logger->log('[钉钉接口]请求自定义机器人消息接口,请求地址:' . json_encode($apiUrl) . ',请求参数:' . json_encode($message) . ',返回结果:' . $body . ' httpcode: ' . $httpCode, 'info'); 45 | 46 | return $body; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Message/Message.php: -------------------------------------------------------------------------------- 1 | 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Kcloze\Jobs\Message; 11 | 12 | use Kcloze\Jobs\Config; 13 | use Kcloze\Jobs\Logs; 14 | 15 | class Message 16 | { 17 | public static function getMessage(array $config) 18 | { 19 | $logger = Logs::getLogger(Config::getConfig()['logPath'] ?? '', Config::getConfig()['logSaveFileApp'] ?? ''); 20 | $classMessage=$config['class'] ?? '\Kcloze\Jobs\Message\DingMessage'; 21 | try { 22 | $message = new $classMessage(); 23 | } catch (\Throwable $e) { 24 | Utils::catchError($logger, $e); 25 | } catch (\Exception $e) { 26 | Utils::catchError($logger, $e); 27 | } 28 | 29 | return $message; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Message/MessageInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Kcloze\Jobs\Message; 11 | 12 | interface MessageInterface 13 | { 14 | public function init(); 15 | 16 | public function send(string $message); 17 | } 18 | -------------------------------------------------------------------------------- /src/Process.php: -------------------------------------------------------------------------------- 1 | 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Kcloze\Jobs; 11 | 12 | use Kcloze\Jobs\Message\Message; 13 | use Kcloze\Jobs\Queue\Queue; 14 | 15 | class Process 16 | { 17 | const CHILD_PROCESS_CAN_RESTART ='staticWorker'; //子进程可以重启,进程个数固定 18 | const CHILD_PROCESS_CAN_NOT_RESTART ='dynamicWorker'; //子进程不可以重启,进程个数根据队列堵塞情况动态分配 19 | const STATUS_RUNNING ='runnning'; //主进程running状态 20 | const STATUS_WAIT ='wait'; //主进程wait状态 21 | const STATUS_STOP ='stop'; //主进程stop状态 22 | const APP_NAME ='swoole-jobs'; //app name 23 | const STATUS_HSET_KEY_HASH ='status'; //status hash名 24 | 25 | public $processName = ':swooleProcessTopicQueueJob'; // 进程重命名, 方便 shell 脚本管理 26 | public $workers = []; 27 | 28 | private $version = '4.0'; 29 | private $excuteTime =600; //子进程最长执行时间,单位:秒 30 | private $queueMaxNum =10; //队列达到一定长度,发送消息提醒 31 | private $queueMaxNumForProcess = 10; //队列达到一定长度,启动动态子进程 32 | private $queueTickTimer =1000 * 10; //一定时间间隔(毫秒)检查队列长度;默认10秒钟 33 | private $messageTickTimer =1000 * 180; //一定时间间隔(毫秒)发送消息提醒;默认3分钟 34 | private $message =[]; //提醒消息内容 35 | private $workerNum =0; //固定分配的子进程个数 36 | private $dynamicWorkerNum =[]; //动态(不能重启)子进程计数,最大数为每个topic配置workerMaxNum,它的个数是动态变化的 37 | private $workersInfo =[]; 38 | private $ppid; 39 | private $config = []; 40 | private $pidFile = 'master.pid'; //pid存放文件 41 | private $pidInfoFile = 'master.info'; //pid 序列化信息 42 | private $pidStatusFile = 'status.info'; //pid status信息 43 | private $status = ''; 44 | private $logger = null; 45 | private $queue = null; 46 | private $topics = null; 47 | private $beginTime = ''; 48 | private $logSaveFileWorker = 'workers.log'; 49 | 50 | public function __construct() 51 | { 52 | $this->config = Config::getConfig(); 53 | $this->logger = Logs::getLogger($this->config['logPath'] ?? '', $this->config['logSaveFileApp'] ?? '', $this->config['system'] ?? ''); 54 | $this->topics =$this->config['job']['topics'] ?? []; 55 | $this->processName = $this->config['processName'] ?? $this->processName; 56 | $this->excuteTime = $this->config['excuteTime'] ?? $this->excuteTime; 57 | $this->queueMaxNum = $this->config['queueMaxNum'] ?? $this->queueMaxNum; 58 | $this->queueMaxNumForProcess = $this->config['queueMaxNumForProcess'] ?? $this->queueMaxNumForProcess; 59 | $this->queueTickTimer = $this->config['queueTickTimer'] ?? $this->queueTickTimer; 60 | $this->messageTickTimer = $this->config['messageTickTimer'] ?? $this->messageTickTimer; 61 | $this->logSaveFileWorker = $this->config['logSaveFileWorker'] ?? $this->logSaveFileWorker; 62 | 63 | $this->beginTime=time(); 64 | //该变量需要在多进程共享 65 | $this->status=self::STATUS_RUNNING; 66 | 67 | if (isset($this->config['pidPath']) && !empty($this->config['pidPath'])) { 68 | //兼容docker部署多个容器共用一个数据目录的问题 69 | $this->config['pidPath']=$this->config['pidPath'] . '/' . Utils::getHostName(); 70 | Utils::mkdir($this->config['pidPath']); 71 | $this->pidFile =$this->config['pidPath'] . '/' . $this->pidFile; 72 | $this->pidInfoFile =$this->config['pidPath'] . '/' . $this->pidInfoFile; 73 | $this->pidStatusFile=$this->config['pidPath'] . '/' . $this->pidStatusFile; 74 | } else { 75 | die('config pidPath must be set!' . PHP_EOL); 76 | } 77 | 78 | /* 79 | * master.pid 文件记录 master 进程 pid, 方便之后进程管理 80 | * 请管理好此文件位置, 使用 systemd 管理进程时会用到此文件 81 | * 判断文件是否存在,并判断进程是否在运行 82 | */ 83 | if (file_exists($this->pidFile)) { 84 | $pid=$this->getMasterData('pid'); 85 | if ($pid) { 86 | //尝试三次确定是否进程还存在,存在就退出 87 | for ($i=0; $i < 3; ++$i) { 88 | if (@\Swoole\Process::kill($pid, 0)) { 89 | die('已有进程运行中,请先结束或重启' . PHP_EOL); 90 | } 91 | sleep(1); 92 | } 93 | } 94 | } 95 | 96 | \Swoole\Process::daemon(); 97 | $this->ppid = getmypid(); 98 | $data['pid'] =$this->ppid; 99 | $data['status']=$this->status; 100 | $this->saveMasterData($data); 101 | //主进程禁用协程 102 | //$this->disableCoroutine(); 103 | $this->setProcessName(self::APP_NAME . ' master ' . $this->ppid . $this->processName); 104 | } 105 | 106 | public function start() 107 | { 108 | $topics = $this->topics; 109 | $this->logger->log('topics: ' . json_encode($topics)); 110 | 111 | if ($topics) { 112 | //遍历topic任务列表 113 | foreach ((array) $topics as $topic) { 114 | if (isset($topic['workerMinNum']) && isset($topic['name'])) { 115 | //每个topic开启最少个进程消费队列 116 | for ($i = 0; $i < $topic['workerMinNum']; ++$i) { 117 | $this->reserveQueue($i, $topic['name'], self::CHILD_PROCESS_CAN_RESTART); 118 | } 119 | } 120 | } 121 | } 122 | 123 | $this->registSignal(); 124 | $this->registTimer(); 125 | } 126 | 127 | /** 128 | * fork子进程消费队列. 129 | * 130 | * @param [type] $num 子进程编号 131 | * @param [type] $topic topic名称 132 | * @param string $type 是否会重启 canRestart|unRestart 133 | */ 134 | public function reserveQueue($num, $topic, $type=self::CHILD_PROCESS_CAN_RESTART) 135 | { 136 | $reserveProcess = new \Swoole\Process(function ($worker) use ($num, $topic, $type) { 137 | $this->checkMpid($worker); 138 | $beginTime=microtime(true); 139 | try { 140 | //设置进程名字 141 | $this->setProcessName($type . ' ' . $topic . ' job ' . $num . ' ' . $this->processName); 142 | $jobs = new Jobs($this->pidInfoFile); 143 | do { 144 | $jobs->run($topic); 145 | $this->status=$this->getMasterData('status'); 146 | $where = (self::STATUS_RUNNING == $this->status) && ($jobs->popNum <= $jobs->maxPopNum) && (self::CHILD_PROCESS_CAN_RESTART == $type ? time() < ($beginTime + $this->excuteTime) : false); 147 | } while ($where); 148 | } catch (\Throwable $e) { 149 | Utils::catchError($this->logger, $e); 150 | } catch (\Exception $e) { 151 | Utils::catchError($this->logger, $e); 152 | } 153 | 154 | $endTime=microtime(true); 155 | $this->logger->log($type . ' ' . $topic . ' worker id: ' . $num . ', pid: ' . getmypid() . ' is done!!! popNum: ' . $jobs->popNum . ', Timing: ' . ($endTime - $beginTime), 'info', $this->logSaveFileWorker); 156 | unset($num, $topic, $type); 157 | }); 158 | 159 | //$this->disableCoroutine($reserveProcess); 160 | 161 | $pid = $reserveProcess->start(); 162 | $this->workers[$pid] = $reserveProcess; 163 | $this->workersInfo[$pid]['type'] = $type; 164 | $this->workersInfo[$pid]['topic'] = $topic; 165 | $this->logger->log('topic: ' . $topic . ' ' . $type . ' worker id: ' . $num . ' pid: ' . $pid . ' is start...', 'info', $this->logSaveFileWorker); 166 | ++$this->workerNum; 167 | } 168 | 169 | //注册信号 170 | public function registSignal() 171 | { 172 | //强制退出 173 | \Swoole\Process::signal(SIGTERM, function ($signo) { 174 | $this->killWorkersAndExitMaster(); 175 | }); 176 | //强制退出 177 | \Swoole\Process::signal(SIGKILL, function ($signo) { 178 | $this->killWorkersAndExitMaster(); 179 | }); 180 | //平滑退出 181 | \Swoole\Process::signal(SIGUSR1, function ($signo) { 182 | $this->waitWorkers(); 183 | }); 184 | //记录进程状态 185 | \Swoole\Process::signal(SIGUSR2, function ($signo) { 186 | $this->logger->log('[master pid: ' . $this->ppid . '] has been received signal' . $signo); 187 | $result=$this->showStatus(); 188 | $this->saveSwooleJobsStatus($result); 189 | //echo $result; 190 | }); 191 | 192 | \Swoole\Process::signal(SIGCHLD, function ($signo) { 193 | while (true) { 194 | try { 195 | $ret = \Swoole\Process::wait(false); 196 | } catch (\Exception $e) { 197 | $this->logger->log('signoError: ' . $signo . $e->getMessage(), 'error', 'error'); 198 | } 199 | if ($ret) { 200 | $pid = $ret['pid']; 201 | $childProcess = $this->workers[$pid]; 202 | $topic = $this->workersInfo[$pid]['topic'] ?? ''; 203 | $this->status=$this->getMasterData('status'); 204 | $topicCanNotRestartNum = $this->dynamicWorkerNum[$topic] ?? 'null'; 205 | $this->logger->log(self::CHILD_PROCESS_CAN_RESTART . '---' . $topic . '***' . $topicCanNotRestartNum . '***' . $this->status . '***' . $this->workersInfo[$pid]['type'] . '***' . $pid, 'info', $this->logSaveFileWorker); 206 | $this->logger->log($pid . ',' . $this->status . ',' . self::STATUS_RUNNING . ',' . $this->workersInfo[$pid]['type'] . ',' . self::CHILD_PROCESS_CAN_RESTART, 'info', $this->logSaveFileWorker); 207 | 208 | //主进程状态为running并且该子进程是可以重启的 209 | if (self::STATUS_RUNNING == $this->status && self::CHILD_PROCESS_CAN_RESTART == $this->workersInfo[$pid]['type']) { 210 | try { 211 | //子进程重启可能失败,必须启动成功之后,再往下执行;最多尝试30次 212 | for ($i=0; $i < 30; ++$i) { 213 | $newPid = $childProcess->start(); 214 | if ($newPid > 0) { 215 | break; 216 | } 217 | sleep(1); 218 | } 219 | if (!$newPid) { 220 | $this->logger->log('静态子进程重启失败,问题有点严重,平滑退出子进程,主进程会跟着退出', 'error', 'error'); 221 | $this->waitWorkers(); 222 | //$this->reserveQueue(0, $topic); 223 | continue; 224 | } 225 | 226 | $this->workers[$newPid] = $childProcess; 227 | $this->workersInfo[$newPid]['type'] = self::CHILD_PROCESS_CAN_RESTART; 228 | $this->workersInfo[$newPid]['topic'] = $topic; 229 | ++$this->workerNum; 230 | $this->logger->log("Worker Restart, kill_signal={$ret['signal']} PID=" . $newPid, 'info', $this->logSaveFileWorker); 231 | } catch (\Throwable $e) { 232 | $this->logger->log('restartErrorThrow' . $e->getMessage(), 'error', 'error'); 233 | } catch (\Exception $e) { 234 | $this->logger->log('restartError: ' . $e->getMessage(), 'error', 'error'); 235 | } 236 | } 237 | //某个topic动态变化的子进程,退出之后个数减少一个 238 | if (self::CHILD_PROCESS_CAN_NOT_RESTART == $this->workersInfo[$pid]['type']) { 239 | --$this->dynamicWorkerNum[$topic]; 240 | } 241 | $this->logger->log("Worker Exit, kill_signal={$ret['signal']} PID=" . $pid, 'info', $this->logSaveFileWorker); 242 | unset($this->workers[$pid], $this->workersInfo[$pid]); 243 | --$this->workerNum; 244 | 245 | $this->logger->log('Worker count: ' . \count($this->workers) . '==' . $this->workerNum, 'info', $this->logSaveFileWorker); 246 | //如果$this->workers为空,且主进程状态为wait,说明所有子进程安全退出,这个时候主进程退出 247 | if (empty($this->workers) && self::STATUS_WAIT == $this->status) { 248 | $this->logger->log('主进程收到所有信号子进程的退出信号,子进程安全退出完成', 'info', $this->logSaveFileWorker); 249 | $this->exitMaster(); 250 | } 251 | } else { 252 | break; 253 | } 254 | } 255 | }); 256 | } 257 | 258 | //增加定时器,检查队列积压情况; 259 | public function registTimer() 260 | { 261 | \Swoole\Timer::tick($this->queueTickTimer, function ($timerId) { 262 | $topics = $this->topics; 263 | $this->status =$this->getMasterData('status'); 264 | if (empty($this->workers) && self::STATUS_WAIT == $this->status) { 265 | $this->exitMaster(); 266 | } 267 | $this->queue = Queue::getQueue($this->config['job']['queue'], $this->logger); 268 | if (empty($this->queue)) { 269 | $this->logger->log('queue connection is lost', 'info', $this->logSaveFileWorker); 270 | 271 | return; 272 | } 273 | $this->queue->setTopics($topics); 274 | 275 | if ($topics && self::STATUS_RUNNING == $this->status) { 276 | //遍历topic任务列表 277 | foreach ((array) $topics as $topic) { 278 | if (empty($topic['name'])) { 279 | continue; 280 | } 281 | $this->dynamicWorkerNum[$topic['name']]=$this->dynamicWorkerNum[$topic['name']] ?? 0; 282 | $topic['workerMaxNum'] =$topic['workerMaxNum'] ?? 0; 283 | 284 | $len=0; 285 | try { 286 | $this->queue = Queue::getQueue($this->config['job']['queue'], $this->logger); 287 | if (empty($this->queue)) { 288 | $this->logger->log('queue connection is lost', 'info', $this->logSaveFileWorker); 289 | 290 | return; 291 | } 292 | $len=$this->queue->len($topic['name']); 293 | $this->logger->log('topic: ' . $topic['name'] . ' ' . $this->status . ' len: ' . $len, 'info', $this->logSaveFileWorker); 294 | } catch (\Throwable $e) { 295 | $this->logger->log('queueError' . $e->getMessage(), 'error', 'error'); 296 | } catch (\Exception $e) { 297 | $this->logger->log('queueError: ' . $e->getMessage(), 'error', 'error'); 298 | } 299 | $this->status=$this->getMasterData('status'); 300 | 301 | //如果当个队列设置了queueMaxNum项,以这个值作为是否警告的阀值; 302 | $queueMaxNum = $topic['queueMaxNum'] ?? $this->queueMaxNum; 303 | //消息提醒:消息体收集 304 | if ($len > $queueMaxNum && \count($this->message) <= \count($topics) && \count($this->message) <= 5) { 305 | $this->message[]= strtr('Hostname: {hostname} Time:{time} Pid:{pid} ProName:{pname} Topic:{topic} Message:{message}' . PHP_EOL . '--------------' . PHP_EOL, [ 306 | '{time}' => date('Y-m-d H:i:s'), 307 | '{pid}' => $this->ppid, 308 | '{hostname}' => gethostname(), 309 | '{pname}' => $this->processName, 310 | '{topic}' => $topic['name'], 311 | '{message}' => '积压消息个数:' . $len, 312 | ]); 313 | } 314 | 315 | //如果当个队列设置了queueMaxNumForProcess项,以这个值作为是否拉起动态子进程的阀值; 316 | $queueMaxNumForProcess = $topic['queueMaxNumForProcess'] ?? $this->queueMaxNumForProcess; 317 | if ($topic['workerMaxNum'] > $topic['workerMinNum'] && self::STATUS_RUNNING == $this->status && $len > $queueMaxNumForProcess && $this->dynamicWorkerNum[$topic['name']] < $topic['workerMaxNum']) { 318 | $max=$topic['workerMaxNum'] - $this->dynamicWorkerNum[$topic['name']]; 319 | for ($i=0; $i < $max; ++$i) { 320 | //队列堆积达到一定数据,拉起一次性子进程,这类进程不会自动重启[没必要] 321 | $this->reserveQueue($this->dynamicWorkerNum[$topic['name']], $topic['name'], self::CHILD_PROCESS_CAN_NOT_RESTART); 322 | ++$this->dynamicWorkerNum[$topic['name']]; 323 | $this->logger->log('topic: ' . $topic['name'] . ' ' . $this->status . ' len: ' . $len . ' for: ' . $i . ' ' . $max, 'info', $this->logSaveFileWorker); 324 | } 325 | } 326 | } 327 | } 328 | //断开连接,释放对象; 329 | $this->queue->close(); 330 | Queue::$_instance=null; 331 | }); 332 | //积压队列提醒 333 | \Swoole\Timer::tick($this->messageTickTimer, function ($timerId) { 334 | !empty($this->message) && $this->logger->log('Warning Message: ' . implode('', $this->message), 'warning', $this->logSaveFileWorker); 335 | if ($this->message && isset($this->config['message'])) { 336 | $message =Message::getMessage($this->config['message']); 337 | $message->send(implode('', $this->message), $this->config['message']['token']); 338 | } 339 | //重置message,防止message不断变长 340 | $this->message=[]; 341 | }); 342 | } 343 | 344 | //平滑等待子进程退出之后,再退出主进程 345 | private function waitWorkers() 346 | { 347 | $data['pid'] =$this->ppid; 348 | $data['status']=self::STATUS_WAIT; 349 | $this->saveMasterData($data); 350 | $this->status = self::STATUS_WAIT; 351 | $this->logger->log('master status: ' . $this->status, 'info', $this->logSaveFileWorker); 352 | } 353 | 354 | //强制杀死子进程并退出主进程 355 | private function killWorkersAndExitMaster() 356 | { 357 | //修改主进程状态为stop 358 | $this->status =self::STATUS_STOP; 359 | if ($this->workers) { 360 | foreach ($this->workers as $pid => $worker) { 361 | //强制杀workers子进程 362 | \Swoole\Process::kill($pid); 363 | unset($this->workers[$pid]); 364 | $this->logger->log('主进程收到退出信号,[' . $pid . ']子进程跟着退出', 'info', $this->logSaveFileWorker); 365 | $this->logger->log('Worker count: ' . \count($this->workers), 'info', $this->logSaveFileWorker); 366 | } 367 | } 368 | $this->exitMaster(); 369 | } 370 | 371 | //退出主进程 372 | private function exitMaster() 373 | { 374 | @unlink($this->pidFile); 375 | @unlink($this->pidInfoFile); 376 | $this->logger->log('Time: ' . microtime(true) . '主进程' . $this->ppid . '退出', 'info', $this->logSaveFileWorker); 377 | 378 | sleep(1); 379 | exit(); 380 | } 381 | 382 | //主进程如果不存在了,子进程退出 383 | private function checkMpid(&$worker) 384 | { 385 | if (!@\Swoole\Process::kill($this->ppid, 0)) { 386 | $worker->exit(); 387 | $this->logger->log("Master process exited, I [{$worker['pid']}] also quit", 'info', $this->logSaveFileWorker); 388 | } 389 | } 390 | 391 | /** 392 | * 设置进程名. 393 | * 394 | * @param mixed $name 395 | */ 396 | private function setProcessName($name) 397 | { 398 | //mac os不支持进程重命名 399 | if (\function_exists('swoole_set_process_name') && PHP_OS != 'Darwin') { 400 | swoole_set_process_name($name); 401 | } 402 | } 403 | 404 | private function saveMasterData($data=[]) 405 | { 406 | isset($data['pid']) && file_put_contents($this->pidFile, $data['pid']); 407 | file_put_contents($this->pidInfoFile, serialize($data)); 408 | } 409 | 410 | private function saveSwooleJobsStatus(string $data) 411 | { 412 | file_put_contents($this->pidStatusFile, $data); 413 | } 414 | 415 | private function getMasterData($key='') 416 | { 417 | $data=unserialize(file_get_contents($this->pidInfoFile)); 418 | if ($key) { 419 | return $data[$key] ?? null; 420 | } 421 | 422 | return $data; 423 | } 424 | 425 | private function showStatus() 426 | { 427 | $statusStr ='-------------------------------------' . $this->processName . ' status--------------------------------------------' . PHP_EOL; 428 | $statusStr .= 'Now: ' . date('Y-m-d H:i:s') . ' PHP version:' . PHP_VERSION . ' Swoole-jobs version: ' . $this->version . PHP_EOL; 429 | $statusStr .= 'start time : ' . date('Y-m-d H:i:s', $this->beginTime) . ' run ' . floor((time() - $this->beginTime) / (24 * 60 * 60)) . ' days ' . floor(((time() - $this->beginTime) % (24 * 60 * 60)) / (60 * 60)) . ' hours ' . PHP_EOL; 430 | $statusStr .= Utils::getSysLoadAvg() . ' memory use:' . Utils::getServerMemoryUsage() . PHP_EOL; 431 | $statusStr .= '|-- Master pid ' . $this->ppid . ' status: ' . $this->status . ' Worker num: ' . \count($this->workers) . PHP_EOL; 432 | if ($this->workers) { 433 | foreach ($this->workers as $pid => $value) { 434 | $type =$this->workersInfo[$pid]['type']; 435 | $topic=$this->workersInfo[$pid]['topic']; 436 | 437 | $statusStr .= ' |---- Worker pid: ' . $pid . ' ' . $type . ' ' . $topic . PHP_EOL; 438 | } 439 | } 440 | 441 | return $statusStr; 442 | } 443 | 444 | private function disableCoroutine(\Swoole\Process $reserveProcess=null) 445 | { 446 | //swoole 4.4.4 447 | if (version_compare(swoole_version(), '4.4.4', '>=')) { 448 | if($reserveProcess instanceof \Swoole\Process){ 449 | $reserveProcess->set(['enable_coroutine' => false]); 450 | } 451 | \Swoole\Timer::set([ 452 | 'enable_coroutine' => false, 453 | ]); 454 | $this->logger->log('Swoole Version >= 4.4.4 ,disable coroutine.', 'info', $this->logSaveFileWorker); 455 | } 456 | } 457 | } 458 | -------------------------------------------------------------------------------- /src/Queue/BaseTopicQueue.php: -------------------------------------------------------------------------------- 1 | 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Kcloze\Jobs\Queue; 11 | 12 | use Kcloze\Jobs\JobObject; 13 | use Kcloze\Jobs\Logs; 14 | 15 | abstract class BaseTopicQueue implements TopicQueueInterface 16 | { 17 | //队列优先级 18 | const HIGH_LEVEL_1=1; 19 | const HIGH_LEVEL_2=2; 20 | const HIGH_LEVEL_3=3; 21 | const HIGH_LEVEL_4=4; 22 | const HIGH_LEVEL_5=5; 23 | 24 | public $topics = []; 25 | public $queue = null; 26 | 27 | public function getTopics() 28 | { 29 | //根据key大到小排序,并保持索引关系 30 | arsort($this->topics); 31 | 32 | return array_values($this->topics); 33 | } 34 | 35 | public function setTopics(array $topics) 36 | { 37 | $this->topics = $topics; 38 | } 39 | 40 | abstract public static function getConnection(array $config, Logs $logger); 41 | 42 | abstract public function push($topic, JobObject $job): string; 43 | 44 | abstract public function pop($topic); 45 | 46 | abstract public function ack(): bool; 47 | 48 | /** 49 | * 清空队列,保留队列名. 50 | * 51 | * @param [type] $topic 52 | */ 53 | abstract public function purge($topic); 54 | 55 | /** 56 | * 删除队列. 57 | * 58 | * @param [type] $topic 59 | */ 60 | abstract public function delete($topic); 61 | 62 | abstract public function len($topic): int; 63 | 64 | abstract public function close(); 65 | 66 | abstract public function isConnected(); 67 | } 68 | -------------------------------------------------------------------------------- /src/Queue/Queue.php: -------------------------------------------------------------------------------- 1 | 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Kcloze\Jobs\Queue; 11 | 12 | use Kcloze\Jobs\Logs; 13 | 14 | class Queue 15 | { 16 | public static $_instance=[]; 17 | 18 | public static function getQueue(array $config, Logs $logger) 19 | { 20 | $classQueue=$config['class'] ?? '\Kcloze\Jobs\Queue\RedisTopicQueue'; 21 | if (is_callable([$classQueue, 'getConnection'])) { 22 | //最多尝试连接3次 23 | for ($i=0; $i < 3; ++$i) { 24 | $connection=static::getInstance($classQueue, $config, $logger); 25 | if ($connection && is_object($connection)) { 26 | // $logger->log('connect...,retry=' . ($i + 1), 'info'); 27 | break; 28 | } 29 | $logger->log('connect...,retry=' . ($i + 1), 'error', 'error'); 30 | } 31 | 32 | return $connection; 33 | } 34 | $logger->log('queue connection is lost', 'error', 'error'); 35 | 36 | return false; 37 | } 38 | 39 | /** 40 | * queue连接实体 单例模式. 41 | * 42 | * @param mixed $class 43 | * @param mixed $config 44 | * @param mixed $logger 45 | * 46 | * @return object 类对象 47 | */ 48 | public static function getInstance($class, $config, $logger) 49 | { 50 | //static $_instance=[]; 51 | $pid =getmypid(); 52 | $key = md5($pid . $class . serialize($config)); 53 | if (!isset(static::$_instance[$key])) { 54 | static::$_instance[$key]=$class::getConnection($config, $logger); 55 | if (!is_object(static::$_instance[$key])) { 56 | //异常抛出 57 | throw new \Exception('class name:' . $class . ' lost connection, please check config!'); 58 | } 59 | } 60 | if (static::$_instance[$key]->isConnected()) { 61 | return static::$_instance[$key]; 62 | } 63 | static::$_instance[$key]=null; 64 | $logger->log('queue instance is null', 'error', 'error'); 65 | 66 | return false; 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/Queue/RabbitmqTopicQueue.php: -------------------------------------------------------------------------------- 1 | 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Kcloze\Jobs\Queue; 11 | 12 | use Enqueue\AmqpExt\AmqpConnectionFactory; 13 | use Enqueue\AmqpExt\AmqpContext; 14 | use Enqueue\AmqpTools\RabbitMqDelayPluginDelayStrategy; 15 | use Enqueue\AmqpTools\RabbitMqDlxDelayStrategy; 16 | use Interop\Amqp\AmqpQueue; 17 | use Interop\Amqp\AmqpTopic; 18 | use Kcloze\Jobs\JobObject; 19 | use Kcloze\Jobs\Logs; 20 | use Kcloze\Jobs\Serialize; 21 | use Kcloze\Jobs\Utils; 22 | 23 | class RabbitmqTopicQueue extends BaseTopicQueue 24 | { 25 | const EXCHANGE ='php.amqp.ext'; 26 | 27 | public $context =null; 28 | private $logger =null; 29 | private $consumer =null; 30 | private $message =null; 31 | 32 | /** 33 | * RabbitmqTopicQueue constructor. 34 | * 使用依赖注入的方式. 35 | * 36 | * @param array $queue 37 | * @param mixed $exchange 38 | */ 39 | public function __construct(AmqpContext $context, $exchange, Logs $logger) 40 | { 41 | $this->logger = $logger; 42 | $rabbitTopic = $context->createTopic($exchange ?? self::EXCHANGE); 43 | $rabbitTopic->addFlag(AmqpTopic::FLAG_DURABLE); 44 | //$rabbitTopic->setType(AmqpTopic::TYPE_FANOUT); 45 | $context->declareTopic($rabbitTopic); 46 | $this->context = $context; 47 | } 48 | 49 | public static function getConnection(array $config, Logs $logger) 50 | { 51 | try { 52 | $factory = new AmqpConnectionFactory($config); 53 | $context = $factory->createContext(); 54 | $connection = new self($context, $config['exchange'] ?? null, $logger); 55 | } catch (\AMQPConnectionException $e) { 56 | Utils::catchError($logger, $e); 57 | 58 | return false; 59 | } catch (\Throwable $e) { 60 | Utils::catchError($logger, $e); 61 | 62 | return false; 63 | } catch (\Exception $e) { 64 | Utils::catchError($logger, $e); 65 | 66 | return false; 67 | } 68 | 69 | return $connection; 70 | } 71 | 72 | /* 73 | * push message to queue. 74 | * 75 | * @param [string] $topic 76 | * @param [JobObject] $job 77 | * @param [int] $delay 延迟毫秒 78 | * @param [int] $priority 优先级 79 | * @param [int] $expiration 过期毫秒 80 | */ 81 | 82 | /** 83 | * push message to queue. 84 | * 85 | * @param [type] $topic 86 | * @param JobObject $job 87 | * @param int $delayStrategy 88 | * @param mixed $serializeFunc 89 | */ 90 | public function push($topic, JobObject $job, $delayStrategy=1, $serializeFunc='php'): string 91 | { 92 | if (!$this->isConnected()) { 93 | return ''; 94 | } 95 | 96 | $queue = $this->createQueue($topic); 97 | if (!\is_object($queue)) { 98 | //对象有误 则直接返回空 99 | return ''; 100 | } 101 | $message = $this->context->createMessage(Serialize::serialize($job, $serializeFunc)); 102 | $producer =$this->context->createProducer(); 103 | $delay = $job->jobExtras['delay'] ?? 0; 104 | $priority = $job->jobExtras['priority'] ?? BaseTopicQueue::HIGH_LEVEL_1; 105 | $expiration = $job->jobExtras['expiration'] ?? 0; 106 | if ($delay > 0) { 107 | //有两种策略实现延迟队列:rabbitmq插件,对消息创建延迟队列;自带队列延迟,变像实现,每个不同的过期时间都会创建队列(不推荐) 108 | if (1 == $delayStrategy) { 109 | $delayStrategyObj= new RabbitMqDelayPluginDelayStrategy(); 110 | } else { 111 | $delayStrategyObj= new RabbitMqDlxDelayStrategy(); 112 | } 113 | $producer->setDelayStrategy($delayStrategyObj); 114 | $producer->setDeliveryDelay($delay); 115 | } 116 | if ($priority) { 117 | $producer->setPriority($priority); 118 | } 119 | if ($expiration > 0) { 120 | $producer->setTimeToLive($expiration); 121 | } 122 | 123 | $result=$producer->send($queue, $message); 124 | 125 | return $job->uuid ?? ''; 126 | } 127 | 128 | /** 129 | * 入队列 . 130 | * 131 | * @param [type] $topic 132 | * @param string $unSerializeFunc 反序列化类型 133 | */ 134 | public function pop($topic, $unSerializeFunc='php') 135 | { 136 | if (!$this->isConnected()) { 137 | return; 138 | } 139 | //reset consumer and message properties 140 | $this->consumer=null; 141 | $this->message =null; 142 | 143 | $queue = $this->createQueue($topic); 144 | $consumer = $this->context->createConsumer($queue); 145 | 146 | if ($m = $consumer->receive(1)) { 147 | $result =$m->getBody(); 148 | $this->consumer =$consumer; 149 | $this->message =$m; 150 | //判断字符串是否是php序列化的字符串,目前只允许serialzie和json两种 151 | $unSerializeFunc=Serialize::isSerial($result) ? 'php' : 'json'; 152 | 153 | return !empty($result) ? Serialize::unserialize($result, $unSerializeFunc) : null; 154 | } 155 | } 156 | 157 | public function ack(): bool 158 | { 159 | if ($this->consumer && $this->message) { 160 | $this->consumer->acknowledge($this->message); 161 | 162 | return true; 163 | } 164 | throw new \Exception(self::get_class() . ' properties consumer or message is null !'); 165 | 166 | return false; 167 | } 168 | 169 | //这里的topic跟rabbitmq不一样,其实就是队列名字 170 | public function len($topic): int 171 | { 172 | if (!$this->isConnected()) { 173 | return 0; 174 | } 175 | 176 | $queue = $this->createQueue($topic); 177 | if (!\is_object($queue)) { 178 | //对象有误 则直接返回空 179 | return -1; 180 | } 181 | $len =$this->context->declareQueue($queue); 182 | 183 | return $len ?? 0; 184 | } 185 | 186 | //清空mq队列数据 187 | public function purge($topic) 188 | { 189 | if (!$this->isConnected()) { 190 | return 0; 191 | } 192 | $queue = $this->createQueue($topic); 193 | 194 | return $this->context->purgeQueue($queue); 195 | } 196 | 197 | //删除mq队列 198 | public function delete($topic) 199 | { 200 | if (!$this->isConnected()) { 201 | return 0; 202 | } 203 | $queue = $this->createQueue($topic); 204 | 205 | return $this->context->deleteQueue($queue); 206 | } 207 | 208 | public function close() 209 | { 210 | if (!$this->isConnected()) { 211 | return; 212 | } 213 | 214 | $this->context->close(); 215 | } 216 | 217 | public function isConnected() 218 | { 219 | return $this->context->getExtChannel()->getConnection()->isConnected(); 220 | } 221 | 222 | private function createQueue($topic) 223 | { 224 | try { 225 | $i = 0; 226 | do { 227 | $queue = $this->context->createQueue($topic); 228 | ++$i; 229 | if (($queue && $this->isConnected()) || $i >= 3) { 230 | //成功 或 链接超过3次则跳出 231 | break; 232 | } 233 | sleep(1); //延迟1秒 234 | } while (!$queue); 235 | $queue->addFlag(AmqpQueue::FLAG_DURABLE); 236 | $len =$this->context->declareQueue($queue); 237 | } catch (\Throwable $e) { 238 | Utils::catchError($this->logger, $e); 239 | 240 | return false; 241 | } catch (\Exception $e) { 242 | Utils::catchError($this->logger, $e); 243 | 244 | return false; 245 | } 246 | 247 | return $queue; 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /src/Queue/RedisTopicQueue.php: -------------------------------------------------------------------------------- 1 | 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Kcloze\Jobs\Queue; 11 | 12 | use Kcloze\Jobs\JobObject; 13 | use Kcloze\Jobs\Logs; 14 | use Kcloze\Jobs\Serialize; 15 | use Kcloze\Jobs\Utils; 16 | 17 | class RedisTopicQueue extends BaseTopicQueue 18 | { 19 | private $logger =null; 20 | 21 | /** 22 | * RedisTopicQueue constructor. 23 | * 使用依赖注入的方式. 24 | * 25 | * @param \Redis $redis 26 | */ 27 | public function __construct(\Redis $redis, Logs $logger) 28 | { 29 | $this->queue = $redis; 30 | $this->logger = $logger; 31 | } 32 | 33 | public static function getConnection(array $config, Logs $logger) 34 | { 35 | try { 36 | $redis = new \Redis(); 37 | $redis->connect($config['host'], $config['port']); 38 | if (isset($config['password']) && !empty($config['password'])) { 39 | $redis->auth($config['password']); 40 | } 41 | } catch (\Throwable $e) { 42 | Utils::catchError($logger, $e); 43 | 44 | return false; 45 | } catch (\Exception $e) { 46 | Utils::catchError($logger, $e); 47 | 48 | return false; 49 | } 50 | $connection = new self($redis, $logger); 51 | 52 | return $connection; 53 | } 54 | 55 | /** 56 | * push message to queue. 57 | * 58 | * @param [type] $topic 59 | * @param JobObject $job 60 | * @param mixed $serializeFunc 61 | * @param mixed $delayStrategy redis这个参数没有用,用于跟rabbitmq传参一致性 62 | */ 63 | public function push($topic, JobObject $job, $delayStrategy=1, $serializeFunc='php'): string 64 | { 65 | if (!$this->isConnected()) { 66 | return ''; 67 | } 68 | 69 | $this->queue->rPush($topic, Serialize::serialize($job, $serializeFunc)); 70 | 71 | return $job->uuid ?? ''; 72 | } 73 | 74 | public function pop($topic, $unSerializeFunc='php') 75 | { 76 | if (!$this->isConnected()) { 77 | return; 78 | } 79 | 80 | $result = $this->queue->lPop($topic); 81 | 82 | //判断字符串是否是php序列化的字符串,目前只允许serialzie和json两种 83 | $unSerializeFunc=Serialize::isSerial($result) ? 'php' : 'json'; 84 | 85 | return !empty($result) ? Serialize::unSerialize($result, $unSerializeFunc) : null; 86 | } 87 | 88 | //redis不支持ack功能,搞个假的,没办法 89 | public function ack(): bool 90 | { 91 | return true; 92 | } 93 | 94 | public function len($topic): int 95 | { 96 | if (!$this->isConnected()) { 97 | return 0; 98 | } 99 | 100 | return (int) $this->queue->lLen($topic) ?? 0; 101 | } 102 | 103 | public function purge($topic) 104 | { 105 | if (!$this->isConnected()) { 106 | return 0; 107 | } 108 | 109 | return (int) $this->queue->ltrim($topic, 1, 0) ?? 0; 110 | } 111 | 112 | public function delete($topic) 113 | { 114 | if (!$this->isConnected()) { 115 | return 0; 116 | } 117 | 118 | return (int) $this->queue->delete($topic) ?? 0; 119 | } 120 | 121 | public function close() 122 | { 123 | if (!$this->isConnected()) { 124 | return; 125 | } 126 | 127 | $this->queue->close(); 128 | } 129 | 130 | public function isConnected() 131 | { 132 | try { 133 | $this->queue->ping(); 134 | } catch (\Throwable $e) { 135 | Utils::catchError($this->logger, $e); 136 | 137 | return false; 138 | } catch (\Exception $e) { 139 | Utils::catchError($this->logger, $e); 140 | 141 | return false; 142 | } 143 | 144 | return true; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/Queue/TopicQueueInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Kcloze\Jobs\Queue; 11 | 12 | use Kcloze\Jobs\JobObject; 13 | use Kcloze\Jobs\Logs; 14 | 15 | interface TopicQueueInterface 16 | { 17 | public static function getConnection(array $config, Logs $logger); 18 | 19 | /** 20 | * @return array a array of topics 21 | */ 22 | public function getTopics(); 23 | 24 | /** 25 | * @param array $topics 26 | */ 27 | public function setTopics(array $topics); 28 | 29 | /** 30 | * 推送队列,返回jobid字符串. 31 | * 32 | * @param [type] $topic 33 | * @param JobObject $job 34 | * 35 | * @return string 36 | */ 37 | public function push($topic, JobObject $job): string; 38 | 39 | /** 40 | * 从队列拿消息. 41 | * 42 | * @param [type] $topic 43 | * 44 | * @return array 45 | */ 46 | public function pop($topic); 47 | 48 | /** 49 | * ack确认消息. 50 | */ 51 | public function ack(): bool; 52 | 53 | /** 54 | * @param $topic 55 | * 56 | * @return int 57 | */ 58 | public function len($topic): int; 59 | 60 | public function close(); 61 | 62 | public function isConnected(); 63 | } 64 | -------------------------------------------------------------------------------- /src/Router.php: -------------------------------------------------------------------------------- 1 | get('/', function () { 11 | echo 'hello,swoole-jobs! '.microtime(); 12 | }); 13 | $router->get('/hello', function () { 14 | echo 'hello,swoole-jobs! '.microtime(); 15 | }); 16 | $router->get('/pushJobs', function () { 17 | $object=new \Kcloze\Jobs\Api\Controller\Index(); 18 | echo $object->push(); 19 | }); 20 | $router->get('/demo', function () { 21 | $object=new \Kcloze\Jobs\Api\Controller\Index(); 22 | echo $object->demo(); 23 | }); 24 | $router->run(); 25 | //unset($router); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Serialize.php: -------------------------------------------------------------------------------- 1 | 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Kcloze\Jobs; 11 | 12 | class Serialize 13 | { 14 | /** 15 | * 序列化. 16 | * 17 | * @param [type] $str 18 | * @param string $serializeFunc php json 19 | */ 20 | public static function serialize($str, $serializeFunc='php') 21 | { 22 | switch ($serializeFunc) { 23 | case 'php': 24 | $str=serialize($str); 25 | break; 26 | case 'json': 27 | $str=json_encode($str); 28 | break; 29 | 30 | default: 31 | $str=serialize($str); 32 | break; 33 | } 34 | 35 | return $str; 36 | } 37 | 38 | /** 39 | * 反序列化. 40 | * 41 | * @param [type] $str 42 | * @param string $unSerializeFunc php json 43 | */ 44 | public static function unSerialize($str, $unSerializeFunc='php') 45 | { 46 | switch ($unSerializeFunc) { 47 | case 'php': 48 | $str=unserialize($str); 49 | break; 50 | case 'json': 51 | $str=json_decode($str, true); 52 | break; 53 | 54 | default: 55 | $str=unserialize($str); 56 | break; 57 | } 58 | 59 | return $str; 60 | } 61 | 62 | /** 63 | * Check if a string is serialized. 64 | * 65 | * @param mixed $str 66 | */ 67 | public static function isSerial($str) 68 | { 69 | return $str == serialize(false) || false !== @unserialize($str); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/TopicConfigObject.php: -------------------------------------------------------------------------------- 1 | initAttributes($config); 22 | } 23 | } 24 | 25 | /** 26 | * @param array $config 27 | */ 28 | public function initAttributes(array $config) 29 | { 30 | $class = new \ReflectionClass($this); 31 | foreach ($class->getProperties() as $property) { 32 | if (isset($config[$property->getName()])) { 33 | $method = 'set' . $property->getName(); 34 | if (method_exists($this, $method)) { 35 | $reflectionMethod = new \ReflectionMethod($this, $method); 36 | $reflectionMethod->setAccessible(true); 37 | $reflectionMethod->invoke($this, $config[$property->getName()]); 38 | } 39 | } 40 | } 41 | } 42 | 43 | /** 44 | * @return string 45 | */ 46 | public function getName() 47 | { 48 | return $this->name; 49 | } 50 | 51 | /** 52 | * @param string $name 53 | */ 54 | public function setName($name) 55 | { 56 | $this->name = $name; 57 | } 58 | 59 | /** 60 | * @return int 61 | */ 62 | public function getWorkerMinNum() 63 | { 64 | return $this->workerMinNum; 65 | } 66 | 67 | /** 68 | * @param int $workerMinNum 69 | */ 70 | public function setWorkerMinNum($workerMinNum) 71 | { 72 | $this->workerMinNum = $workerMinNum; 73 | } 74 | 75 | /** 76 | * @return int 77 | */ 78 | public function getWorkerMaxNum() 79 | { 80 | return $this->workerMaxNum; 81 | } 82 | 83 | /** 84 | * @param int $workerMaxNum 85 | */ 86 | public function setWorkerMaxNum($workerMaxNum) 87 | { 88 | $this->workerMaxNum = $workerMaxNum; 89 | } 90 | 91 | /** 92 | * @return int 93 | */ 94 | public function getQueueMaxNum() 95 | { 96 | return $this->queueMaxNum; 97 | } 98 | 99 | /** 100 | * @param int $queueMaxNum 101 | */ 102 | public function setQueueMaxNum($queueMaxNum) 103 | { 104 | $this->queueMaxNum = $queueMaxNum; 105 | } 106 | 107 | /** 108 | * @return int 109 | */ 110 | public function getQueueMaxNumForProcess() 111 | { 112 | return $this->queueMaxNumForProcess; 113 | } 114 | 115 | /** 116 | * @param int $queueMaxNumForProcess 117 | */ 118 | public function setQueueMaxNumForProcess($queueMaxNumForProcess) 119 | { 120 | $this->queueMaxNumForProcess = $queueMaxNumForProcess; 121 | } 122 | 123 | /** 124 | * @return string 125 | */ 126 | public function getDefaultJobClass() 127 | { 128 | return $this->defaultJobClass; 129 | } 130 | 131 | /** 132 | * @param string $defaultJobClass 133 | */ 134 | public function setDefaultJobClass($defaultJobClass) 135 | { 136 | $this->defaultJobClass = $defaultJobClass; 137 | } 138 | 139 | /** 140 | * @return string 141 | */ 142 | public function getDefaultJobMethod() 143 | { 144 | return $this->defaultJobMethod; 145 | } 146 | 147 | /** 148 | * @param string $defaultJobMethod 149 | */ 150 | public function setDefaultJobMethod($defaultJobMethod) 151 | { 152 | $this->defaultJobMethod = $defaultJobMethod; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/Utils.php: -------------------------------------------------------------------------------- 1 | 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | namespace Kcloze\Jobs; 11 | 12 | class Utils 13 | { 14 | /** 15 | * 循环创建目录. 16 | * 17 | * @param mixed $path 18 | * @param mixed $recursive 19 | * @param mixed $mode 20 | */ 21 | public static function mkdir($path, $mode=0777, $recursive=true) 22 | { 23 | if (!is_dir($path)) { 24 | mkdir($path, $mode, $recursive); 25 | } 26 | } 27 | 28 | public static function catchError(Logs $logger, $exception) 29 | { 30 | $error = '错误类型:' . \get_class($exception) . PHP_EOL; 31 | $error .= '错误代码:' . $exception->getCode() . PHP_EOL; 32 | $error .= '错误信息:' . $exception->getMessage() . PHP_EOL; 33 | $error .= '错误堆栈:' . $exception->getTraceAsString() . PHP_EOL; 34 | 35 | $logger && $logger->log($error, 'error', 'error'); 36 | } 37 | 38 | public static function getMillisecond() 39 | { 40 | return microtime(true); 41 | } 42 | 43 | /** 44 | * Get Server Memory Usage. 45 | * 46 | * @return string 47 | */ 48 | public static function getServerMemoryUsage() 49 | { 50 | return round(memory_get_usage(true) / (1024 * 1024), 2) . ' MB'; 51 | } 52 | 53 | /** 54 | * Get Server load avg. 55 | * 56 | * @return string 57 | */ 58 | public static function getSysLoadAvg() 59 | { 60 | $loadavg = \function_exists('sys_getloadavg') ? array_map('round', sys_getloadavg(), [2]) : ['-', '-', '-']; 61 | 62 | return 'load average: ' . implode(', ', $loadavg); 63 | } 64 | 65 | //获取机器名称 66 | public static function getHostName() 67 | { 68 | $hostname=gethostname(); 69 | if (empty($hostname)) { 70 | $hostname='system'; 71 | } 72 | 73 | return strtolower(trim($hostname)); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tests/DefaultJobClassMethodConfigTest.php: -------------------------------------------------------------------------------- 1 | config= require SWOOLE_JOBS_ROOT_PATH . '/config.php'; 14 | \Kcloze\Jobs\Config::setConfig($this->config); 15 | $logger = \Kcloze\Jobs\Logs::getLogger($this->config['logPath'] ?? '', $this->config['logSaveFileApp'] ?? ''); 16 | $this->queue =\Kcloze\Jobs\Queue\Queue::getQueue($this->config['job']['queue'], $logger); 17 | } 18 | 19 | public function testBase() 20 | { 21 | $this->assertSame('\\'.get_class($this->queue), $this->config['job']['queue']['class']); 22 | $topicName = 'DefaultClassMethod.test1'; 23 | $this->queue->delete($topicName); 24 | $jobObject = new \Kcloze\Jobs\JobObject($topicName, '', '', ['functionName'=>__FUNCTION__, 'timestamp'=>time()]); 25 | $this->assertNotEmpty($this->queue->push($topicName, $jobObject, 1, 'json')); 26 | $this->assertSame(1, $this->queue->len($topicName)); 27 | $messageBody = $this->queue->pop($topicName, 'json'); 28 | $this->assertNotEmpty($messageBody); 29 | $this->assertSame($jobObject->topic, $messageBody['topic']); 30 | $this->assertSame($jobObject->jobClass, $messageBody['jobClass']); 31 | $this->assertSame($jobObject->jobMethod, $messageBody['jobMethod']); 32 | $this->assertSame($jobObject->jobParams, $messageBody['jobParams']); 33 | $this->assertSame($jobObject->jobExtras, $messageBody['jobExtras']); 34 | } 35 | 36 | public function testDefault() 37 | { 38 | $topicName = 'DefaultClassMethod.test1'; 39 | $this->queue->delete($topicName); 40 | $jobParams = ['orderNo'=>'12345678910', 'userId'=>'9527', 'userName'=>'凌凌漆', 'paymentTime'=>time()]; 41 | $jobObject = new \Kcloze\Jobs\JobObject( 42 | $topicName, 43 | '', 44 | '', 45 | $jobParams 46 | ); 47 | $this->assertNotEmpty($this->queue->push($topicName, $jobObject, 1, 'json')); 48 | $this->assertSame(1, $this->queue->len($topicName)); 49 | $messageBody = $this->queue->pop($topicName, 'json'); 50 | $this->assertNotEmpty($messageBody); 51 | $this->assertSame($jobObject->topic, $messageBody['topic']); 52 | $this->assertSame($jobObject->jobClass, $messageBody['jobClass']); 53 | $this->assertSame($jobObject->jobMethod, $messageBody['jobMethod']); 54 | $this->assertSame($jobObject->jobParams, $messageBody['jobParams']); 55 | $this->assertSame($jobObject->jobParams, $jobParams); 56 | $this->assertSame($jobObject->jobExtras, $messageBody['jobExtras']); 57 | 58 | $job = new \Kcloze\Jobs\Jobs(''); 59 | $config = $job->getConfigByTopic($topicName); 60 | $jobObject = $job->formatJobObjectByTopicConfig($jobObject, $topicName,[]); 61 | $this->assertNotEmpty($jobObject->topic); 62 | $this->assertNotEmpty($jobObject->jobClass); 63 | $this->assertNotEmpty($jobObject->jobMethod); 64 | $this->assertSame($jobObject->topic, $topicName); 65 | $this->assertSame($jobObject->jobParams, $jobParams); 66 | $this->assertSame($jobObject->jobParams, $messageBody['jobParams']); 67 | $this->assertSame($jobObject->jobExtras, $messageBody['jobExtras']); 68 | $this->assertSame($jobObject->topic, $config['name']); 69 | $this->assertSame($jobObject->jobClass, $config['defaultJobClass']); 70 | $this->assertSame($jobObject->jobMethod, $config['defaultJobMethod']); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/QueueTest.php: -------------------------------------------------------------------------------- 1 | 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | define('SWOOLE_JOBS_ROOT_PATH', __DIR__ . '/..'); 11 | 12 | use Kcloze\Jobs\Config; 13 | use Kcloze\Jobs\JobObject; 14 | use Kcloze\Jobs\Logs; 15 | use Kcloze\Jobs\Queue\BaseTopicQueue; 16 | use Kcloze\Jobs\Queue\Queue; 17 | use PHPUnit\Framework\TestCase; 18 | 19 | class QueueTest extends TestCase 20 | { 21 | private $queue =null; 22 | private $config=null; 23 | 24 | public function __construct() 25 | { 26 | $this->config = require SWOOLE_JOBS_ROOT_PATH . '/config.php'; 27 | $logger = Logs::getLogger($this->config['logPath'] ?? '', $this->config['logSaveFileApp'] ?? ''); 28 | $this->queue =Queue::getQueue($this->config['job']['queue'], $logger); 29 | } 30 | 31 | public function testQueue() 32 | { 33 | $this->assertInternalType('object', $this->queue); 34 | 35 | $len =$this->queue->len('MyJob'); 36 | 37 | $rand =mt_rand(0, 100); 38 | $delay =$rand * 1000; 39 | $priority =BaseTopicQueue::HIGH_LEVEL_1; 40 | // $jobExtras['delay'] =$delay; 41 | // $jobExtras['priority'] =$priority; 42 | $job =new JobObject('MyJob', '\Kcloze\Jobs\Jobs\MyJob', 'test1', ['kcloze', time()]); 43 | $result =$this->queue->push('MyJob', $job, 1, 'json'); 44 | $len2 =$this->queue->len('MyJob'); 45 | $this->assertGreaterThan($len, $len2); 46 | //删除队列 47 | $this->queue->purge('MyJob'); 48 | $len = $this->queue->len('MyJob'); 49 | $this->assertSame(0, $len); 50 | //清空队列 51 | $result = $this->queue->push('MyJob', $job, 1, 'json'); 52 | $this->queue->delete('MyJob'); 53 | $len =$this->queue->len('MyJob'); 54 | $this->assertSame(0, $len); 55 | } 56 | 57 | public function testPushAndPop() 58 | { 59 | $stack = []; 60 | $this->assertSame(0, count($stack)); 61 | 62 | array_push($stack, 'foo'); 63 | $this->assertSame('foo', $stack[count($stack) - 1]); 64 | $this->assertSame(1, count($stack)); 65 | 66 | $this->assertSame('foo', array_pop($stack)); 67 | $this->assertSame(0, count($stack)); 68 | } 69 | 70 | public function testConfig() 71 | { 72 | $topics=$this->config['job']['topics']; 73 | $this->assertFalse(Config::getTopicConfig($topics, 'MyJob', 'autoAckBeforeJobStart')); 74 | $this->assertTrue(Config::getTopicConfig($topics, 'MyJob2', 'autoAckBeforeJobStart')); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tests/TopicConfigObjectTest.php: -------------------------------------------------------------------------------- 1 | [ 11 | 'a', 'b', 'c', 12 | ], 13 | 'attribute'=> null, 14 | ]; 15 | $object = new \Kcloze\Jobs\TopicConfigObject(); 16 | $object->initAttributes($config); 17 | $this->assertSame('', $object->getName()); 18 | $this->assertSame('', $object->getDefaultJobClass()); 19 | $this->assertSame('', $object->getDefaultJobMethod()); 20 | $this->assertNull($object->getWorkerMinNum()); 21 | $this->assertNull($object->getWorkerMaxNum()); 22 | $this->assertNull($object->getQueueMaxNum()); 23 | $this->assertNull($object->getQueueMaxNumForProcess()); 24 | } 25 | 26 | public function testAttributes() 27 | { 28 | $config = [ 29 | 'name' => 'nameValue', 30 | 'defaultJobClass' => 'jobClassValue', 31 | 'defaultJobMethod' => 'jobMethodValue', 32 | 'workerMinNum' => 1, 33 | 'workerMaxNum' => 3, 34 | 'queueMaxNum' => 10, 35 | 'queueMaxNumForProcess' => 10, 36 | 'params' => [ 37 | 'a', 'b', 'c', 38 | ], 39 | 'attribute'=> null, 40 | ]; 41 | $object = new \Kcloze\Jobs\TopicConfigObject(); 42 | $object->initAttributes($config); 43 | $this->assertSame($config['name'], $object->getName()); 44 | $this->assertSame($config['defaultJobClass'], $object->getDefaultJobClass()); 45 | $this->assertSame($config['defaultJobMethod'], $object->getDefaultJobMethod()); 46 | $this->assertSame($config['workerMinNum'], $object->getWorkerMinNum()); 47 | $this->assertSame($config['workerMaxNum'], $object->getWorkerMaxNum()); 48 | $this->assertSame($config['queueMaxNum'], $object->getQueueMaxNum()); 49 | $this->assertSame($config['queueMaxNumForProcess'], $object->getQueueMaxNumForProcess()); 50 | } 51 | 52 | public function testConstruct() 53 | { 54 | $config = [ 55 | 'name' => 'nameValue', 56 | 'defaultJobClass' => 'jobClassValue', 57 | 'defaultJobMethod' => 'jobMethodValue', 58 | 'workerMinNum' => 1, 59 | 'workerMaxNum' => 3, 60 | 'queueMaxNum' => 10, 61 | 'queueMaxNumForProcess' => 10, 62 | 'params' => [ 63 | 'a', 'b', 'c', 64 | ], 65 | 'attribute'=> null, 66 | ]; 67 | $object = new \Kcloze\Jobs\TopicConfigObject($config); 68 | $this->assertSame($config['name'], $object->getName()); 69 | $this->assertSame($config['defaultJobClass'], $object->getDefaultJobClass()); 70 | $this->assertSame($config['defaultJobMethod'], $object->getDefaultJobMethod()); 71 | $this->assertSame($config['workerMinNum'], $object->getWorkerMinNum()); 72 | $this->assertSame($config['workerMaxNum'], $object->getWorkerMaxNum()); 73 | $this->assertSame($config['queueMaxNum'], $object->getQueueMaxNum()); 74 | $this->assertSame($config['queueMaxNumForProcess'], $object->getQueueMaxNumForProcess()); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tests/testCache.php: -------------------------------------------------------------------------------- 1 | 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | define('SWOOLE_JOBS_ROOT_PATH', __DIR__ . '/..'); 11 | 12 | date_default_timezone_set('Asia/Shanghai'); 13 | 14 | require SWOOLE_JOBS_ROOT_PATH . '/vendor/autoload.php'; 15 | 16 | use Kcloze\Jobs\Cache; 17 | 18 | $config=[ 19 | 'host' => '127.0.0.1', 20 | 'port' => 6379, 21 | ]; 22 | $cache=new Cache($config); 23 | 24 | $cache->set('status', 'running'); 25 | -------------------------------------------------------------------------------- /tests/testJobsJson.php: -------------------------------------------------------------------------------- 1 | 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | define('SWOOLE_JOBS_ROOT_PATH', __DIR__ . '/..'); 11 | 12 | date_default_timezone_set('Asia/Shanghai'); 13 | 14 | require SWOOLE_JOBS_ROOT_PATH . '/vendor/autoload.php'; 15 | 16 | use Kcloze\Jobs\JobObject; 17 | use Kcloze\Jobs\Logs; 18 | use Kcloze\Jobs\Queue\BaseTopicQueue; 19 | use Kcloze\Jobs\Queue\Queue; 20 | 21 | $config = require_once SWOOLE_JOBS_ROOT_PATH . '/config.php'; 22 | $logger = Logs::getLogger($config['logPath'] ?? '', $config['logSaveFileApp'] ?? ''); 23 | $queue =Queue::getQueue($config['job']['queue'], $logger); 24 | 25 | //var_dump($queue); 26 | 27 | $queue->setTopics($config['job']['topics']); 28 | 29 | if (!$queue) { 30 | die("queue object is null\n"); 31 | } 32 | 33 | //jobs的topic需要在配置文件里面定义,并且一次性注册进去 34 | $topics = $queue->getTopics(); 35 | //var_dump($topics); exit; 36 | 37 | addTest1($queue); 38 | addTest2($queue); 39 | addTest3($queue); 40 | addTest4($queue); 41 | 42 | //往topic为MyJob的任务增加执行job 43 | function addTest1($queue) 44 | { 45 | for ($i = 0; $i < 1000; ++$i) { 46 | $rand =mt_rand(0, 100); 47 | $delay =$rand * 1000; 48 | $priority =BaseTopicQueue::HIGH_LEVEL_1; 49 | $jobExtras['delay'] =$delay; 50 | $jobExtras['priority'] =$priority; 51 | $job =new JobObject('MyJob', '\Kcloze\Jobs\Jobs\MyJob', 'test1', ['kcloze', time()], $jobExtras); 52 | $result =$queue->push('MyJob', $job, 1, 'json'); 53 | var_dump($result); 54 | } 55 | } 56 | 57 | function addTest2($queue) 58 | { 59 | for ($i = 0; $i < 100; ++$i) { 60 | $rand =mt_rand(0, 100); 61 | $delay =$rand * 1000; 62 | $priority =BaseTopicQueue::HIGH_LEVEL_2; 63 | $jobExtras['delay'] =$delay; 64 | $jobExtras['priority'] =$priority; 65 | $job =new JobObject('MyJob', '\Kcloze\Jobs\Jobs\MyJob', 'test2', ['kcloze', time(), 'oop'], $jobExtras); 66 | $result =$queue->push('MyJob', $job, 1, 'json'); 67 | var_dump($result); 68 | } 69 | } 70 | 71 | function addTest3($queue) 72 | { 73 | for ($i = 0; $i < 100; ++$i) { 74 | $rand =mt_rand(0, 100); 75 | $delay =$rand * 1000; 76 | $priority =BaseTopicQueue::HIGH_LEVEL_3; 77 | $jobExtras['delay'] =$delay; 78 | $jobExtras['priority'] =$priority; 79 | $job =new JobObject('MyJob', '\Kcloze\Jobs\Jobs\MyJob', 'testError', ['kcloze', time()], $jobExtras); 80 | $result =$queue->push('MyJob', $job, 1, 'json'); 81 | var_dump($result); 82 | } 83 | } 84 | 85 | function addTest4($queue) 86 | { 87 | for ($i = 0; $i < 100; ++$i) { 88 | $rand =mt_rand(0, 100); 89 | $delay =$rand * 1000; 90 | $priority =BaseTopicQueue::HIGH_LEVEL_2; 91 | $jobExtras['delay'] =$delay; 92 | $jobExtras['priority'] =$priority; 93 | $job =new JobObject('MyJob2', '\Kcloze\Jobs\Jobs\MyJob2', 'test1', ['kcloze', time()], $jobExtras); 94 | $result =$queue->push('MyJob2', $job, 1, 'json'); 95 | var_dump($result); 96 | } 97 | for ($i = 0; $i < 100; ++$i) { 98 | $rand =mt_rand(0, 100); 99 | $delay =$rand * 1000; 100 | $priority =BaseTopicQueue::HIGH_LEVEL_2; 101 | $jobExtras['delay'] =$delay; 102 | $jobExtras['priority'] =$priority; 103 | $job =new JobObject('MyJob2', '\Kcloze\Jobs\Jobs\MyJob2', 'test2', ['kcloze', time(), 'oop'], $jobExtras); 104 | $result =$queue->push('MyJob2', $job, 1, 'json'); 105 | var_dump($result); 106 | } 107 | for ($i = 0; $i < 100; ++$i) { 108 | $rand =mt_rand(0, 100); 109 | $delay =$rand * 1000; 110 | $priority =BaseTopicQueue::HIGH_LEVEL_2; 111 | $jobExtras['delay'] =$delay; 112 | $jobExtras['priority'] =$priority; 113 | $job =new JobObject('MyJob2', '\Kcloze\Jobs\Jobs\MyJob2', 'testError', ['kcloze', time()], $jobExtras); 114 | $result =$queue->push('MyJob2', $job, 1, 'json'); 115 | var_dump($result); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /tests/testJobsSerialzie.php: -------------------------------------------------------------------------------- 1 | 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | define('SWOOLE_JOBS_ROOT_PATH', __DIR__ . '/..'); 11 | 12 | date_default_timezone_set('Asia/Shanghai'); 13 | 14 | require SWOOLE_JOBS_ROOT_PATH . '/vendor/autoload.php'; 15 | 16 | use Kcloze\Jobs\JobObject; 17 | use Kcloze\Jobs\Logs; 18 | use Kcloze\Jobs\Queue\BaseTopicQueue; 19 | use Kcloze\Jobs\Queue\Queue; 20 | 21 | $config = require_once SWOOLE_JOBS_ROOT_PATH . '/config.php'; 22 | $logger = Logs::getLogger($config['logPath'] ?? '', $config['logSaveFileApp'] ?? ''); 23 | $queue =Queue::getQueue($config['job']['queue'], $logger); 24 | 25 | //var_dump($queue); 26 | 27 | $queue->setTopics($config['job']['topics']); 28 | 29 | if (!$queue) { 30 | die("queue object is null\n"); 31 | } 32 | 33 | //jobs的topic需要在配置文件里面定义,并且一次性注册进去 34 | $topics = $queue->getTopics(); 35 | //var_dump($topics); exit; 36 | $times=10; 37 | addTest1($queue, $times); 38 | addTest2($queue, $times); 39 | addTest3($queue, $times); 40 | addTest4($queue, $times); 41 | 42 | //往topic为MyJob的任务增加执行job 43 | function addTest1($queue, $times) 44 | { 45 | for ($i = 0; $i < $times; ++$i) { 46 | $rand =mt_rand(0, 100); 47 | $delay =$rand * 1000; 48 | $priority =BaseTopicQueue::HIGH_LEVEL_1; 49 | ////$jobExtras['delay'] =$delay; 50 | $jobExtras['priority'] =$priority; 51 | $job =new JobObject('MyJob', '\Kcloze\Jobs\Jobs\MyJob', 'test1', ['kcloze', time()], $jobExtras); 52 | // var_dump($job); 53 | // exit; 54 | $result =$queue->push('MyJob', $job, 1, 'php'); 55 | var_dump($result); 56 | } 57 | } 58 | 59 | function addTest2($queue, $times) 60 | { 61 | for ($i = 0; $i < $times; ++$i) { 62 | $rand =mt_rand(0, 100); 63 | $delay =$rand * 1000; 64 | $priority =BaseTopicQueue::HIGH_LEVEL_2; 65 | //$jobExtras['delay'] =$delay; 66 | $jobExtras['priority'] =$priority; 67 | $job =new JobObject('MyJob', '\Kcloze\Jobs\Jobs\MyJob', 'test2', ['kcloze', time(), 'oop'], $jobExtras); 68 | $result =$queue->push('MyJob', $job, 1, 'php'); 69 | var_dump($result); 70 | } 71 | } 72 | 73 | function addTest3($queue, $times) 74 | { 75 | for ($i = 0; $i < $times; ++$i) { 76 | $rand =mt_rand(0, 100); 77 | $delay =$rand * 1000; 78 | $priority =BaseTopicQueue::HIGH_LEVEL_3; 79 | //$jobExtras['delay'] =$delay; 80 | $jobExtras['priority'] =$priority; 81 | $job =new JobObject('MyJob', '\Kcloze\Jobs\Jobs\MyJob', 'testError', ['kcloze', time()], $jobExtras); 82 | $result =$queue->push('MyJob', $job, 1, 'php'); 83 | var_dump($result); 84 | } 85 | } 86 | 87 | function addTest4($queue, $times) 88 | { 89 | for ($i = 0; $i < $times; ++$i) { 90 | $rand =mt_rand(0, 100); 91 | $delay =$rand * 1000; 92 | $priority =BaseTopicQueue::HIGH_LEVEL_2; 93 | //$jobExtras['delay'] =$delay; 94 | $jobExtras['priority'] =$priority; 95 | $job =new JobObject('MyJob2', '\Kcloze\Jobs\Jobs\MyJob2', 'test1', ['kcloze', time()], $jobExtras); 96 | $result =$queue->push('MyJob2', $job, 1, 'php'); 97 | var_dump($result); 98 | } 99 | for ($i = 0; $i < $times; ++$i) { 100 | $rand =mt_rand(0, 100); 101 | $delay =$rand * 1000; 102 | $priority =BaseTopicQueue::HIGH_LEVEL_2; 103 | //$jobExtras['delay'] =$delay; 104 | $jobExtras['priority'] =$priority; 105 | $job =new JobObject('MyJob2', '\Kcloze\Jobs\Jobs\MyJob2', 'test2', ['kcloze', time(), 'oop'], $jobExtras); 106 | $result =$queue->push('MyJob2', $job, 1, 'php'); 107 | var_dump($result); 108 | } 109 | for ($i = 0; $i < $times; ++$i) { 110 | $rand =mt_rand(0, 100); 111 | $delay =$rand * 1000; 112 | $priority =BaseTopicQueue::HIGH_LEVEL_2; 113 | //$jobExtras['delay'] =$delay; 114 | $jobExtras['priority'] =$priority; 115 | $job =new JobObject('MyJob2', '\Kcloze\Jobs\Jobs\MyJob2', 'testError', ['kcloze', time()], $jobExtras); 116 | $result =$queue->push('MyJob2', $job, 1, 'php'); 117 | var_dump($result); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /tests/testMessage.php: -------------------------------------------------------------------------------- 1 | 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | // $pid =112; 11 | // $status_str .= "$pid\t" . str_pad('N/A', 7) . ' ' 12 | // //. str_pad($info['listen'], static::$_maxSocketNameLength) . ' ' 13 | // //. str_pad($info['name'], static::$_maxWorkerNameLength) . ' ' 14 | // . str_pad('N/A', 11) . ' ' . str_pad('N/A', 9) . ' ' 15 | // . str_pad('N/A', 7) . ' ' . str_pad('N/A', 13) . " N/A [busy] \n"; 16 | 17 | // echo $status_str; 18 | 19 | define('SWOOLE_JOBS_ROOT_PATH', __DIR__ . '/..'); 20 | 21 | date_default_timezone_set('Asia/Shanghai'); 22 | 23 | require SWOOLE_JOBS_ROOT_PATH . '/vendor/autoload.php'; 24 | 25 | use Kcloze\Jobs\Config; 26 | use Kcloze\Jobs\Message\Message; 27 | 28 | $config = require_once SWOOLE_JOBS_ROOT_PATH . '/config.php'; 29 | Config::setConfig($config); 30 | 31 | $content ='测试机器人吧'; 32 | $message =Message::getMessage($config['message']); 33 | $ret =$message->send($content, $config['message']['token']); 34 | var_dump($ret); 35 | -------------------------------------------------------------------------------- /tests/testRabbitmqConsume.php: -------------------------------------------------------------------------------- 1 | 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | define('SWOOLE_JOBS_ROOT_PATH', __DIR__ . '/..'); 11 | 12 | date_default_timezone_set('Asia/Shanghai'); 13 | 14 | require SWOOLE_JOBS_ROOT_PATH . '/vendor/autoload.php'; 15 | 16 | use Enqueue\AmqpExt\AmqpConnectionFactory; 17 | use Interop\Amqp\AmqpQueue; 18 | use Interop\Amqp\AmqpTopic; 19 | use Interop\Amqp\Impl\AmqpBind; 20 | 21 | $config = require_once SWOOLE_JOBS_ROOT_PATH . '/config.php'; 22 | 23 | $factory = new AmqpConnectionFactory($config['job']['queue']); 24 | $context = $factory->createContext(); 25 | 26 | $topic = $context->createTopic($config['job']['queue']['exchange']); 27 | $topic->addFlag(AmqpTopic::FLAG_DURABLE); 28 | $topic->setType(AmqpTopic::TYPE_FANOUT); 29 | //$topic->setArguments(['alternate-exchange' => 'foo']); 30 | 31 | $context->deleteTopic($topic); 32 | $context->declareTopic($topic); 33 | 34 | while (true) { 35 | $fooQueue = $context->createQueue('foo'); 36 | $fooQueue->addFlag(AmqpQueue::FLAG_DURABLE); 37 | $count =$context->declareQueue($fooQueue); 38 | 39 | $consumer = $context->createConsumer($fooQueue); 40 | if ($m = $consumer->receive(1)) { 41 | $result=$m->getBody(); 42 | $consumer->acknowledge($m); 43 | } 44 | 45 | var_dump($count, $result); 46 | sleep(2); 47 | } 48 | 49 | // $context->deleteQueue($fooQueue); 50 | // $context->declareQueue($fooQueue); 51 | 52 | // $context->bind(new AmqpBind($topic, $fooQueue)); 53 | 54 | // $barQueue = $context->createQueue('bar'); 55 | // $barQueue->addFlag(AmqpQueue::FLAG_DURABLE); 56 | 57 | // $context->deleteQueue($barQueue); 58 | // $context->declareQueue($barQueue); 59 | 60 | // $context->bind(new AmqpBind($topic, $barQueue)); 61 | 62 | // $message = $context->createMessage('Hello Bar!'); 63 | 64 | // while (true) { 65 | // $context->createProducer()->send($fooQueue, $message); 66 | // $context->createProducer()->send($barQueue, $message); 67 | // } 68 | 69 | echo 'Done' . "\n"; 70 | -------------------------------------------------------------------------------- /tests/testRabbitmqProducer.php: -------------------------------------------------------------------------------- 1 | 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | define('SWOOLE_JOBS_ROOT_PATH', __DIR__ . '/..'); 11 | 12 | date_default_timezone_set('Asia/Shanghai'); 13 | 14 | require SWOOLE_JOBS_ROOT_PATH . '/vendor/autoload.php'; 15 | 16 | use Enqueue\AmqpExt\AmqpConnectionFactory; 17 | use Interop\Amqp\AmqpQueue; 18 | use Interop\Amqp\AmqpTopic; 19 | use Interop\Amqp\Impl\AmqpBind; 20 | 21 | $config = require_once SWOOLE_JOBS_ROOT_PATH . '/config.php'; 22 | 23 | $factory = new AmqpConnectionFactory($config['job']['queue']); 24 | $context = $factory->createContext(); 25 | 26 | $topic = $context->createTopic($config['job']['queue']['exchange']); 27 | $topic->addFlag(AmqpTopic::FLAG_DURABLE); 28 | $topic->setType(AmqpTopic::TYPE_FANOUT); 29 | //$topic->setArguments(['alternate-exchange' => 'foo']); 30 | 31 | $context->deleteTopic($topic); 32 | $context->declareTopic($topic); 33 | 34 | $message = $context->createMessage('Hello Bar!'); 35 | $message->setExpiration(60 * 1000); //6 sec 36 | $message->setPriority(5); 37 | $message->setTimestamp(5 * 1000); //5 sec 38 | 39 | //var_dump($message); exit; 40 | 41 | while (true) { 42 | $fooQueue = $context->createQueue('foo'); 43 | $fooQueue->addFlag(AmqpQueue::FLAG_DURABLE); 44 | $count =$context->declareQueue($fooQueue); 45 | $result=$context->createProducer()->send($fooQueue, $message); 46 | var_dump($count, $result); 47 | sleep(2); 48 | } 49 | 50 | // $context->deleteQueue($fooQueue); 51 | // $context->declareQueue($fooQueue); 52 | 53 | // $context->bind(new AmqpBind($topic, $fooQueue)); 54 | 55 | // $barQueue = $context->createQueue('bar'); 56 | // $barQueue->addFlag(AmqpQueue::FLAG_DURABLE); 57 | 58 | // $context->deleteQueue($barQueue); 59 | // $context->declareQueue($barQueue); 60 | 61 | // $context->bind(new AmqpBind($topic, $barQueue)); 62 | 63 | // $message = $context->createMessage('Hello Bar!'); 64 | 65 | // while (true) { 66 | // $context->createProducer()->send($fooQueue, $message); 67 | // $context->createProducer()->send($barQueue, $message); 68 | // } 69 | 70 | echo 'Done' . "\n"; 71 | -------------------------------------------------------------------------------- /tests/testSerialize.php: -------------------------------------------------------------------------------- 1 | 6 | * This source file is subject to the MIT license that is bundled 7 | * with this source code in the file LICENSE. 8 | */ 9 | 10 | define('SWOOLE_JOBS_ROOT_PATH', __DIR__ . '/..'); 11 | 12 | date_default_timezone_set('Asia/Shanghai'); 13 | 14 | require SWOOLE_JOBS_ROOT_PATH . '/vendor/autoload.php'; 15 | 16 | use Kcloze\Jobs\JobObject; 17 | use Kcloze\Jobs\Serialize; 18 | 19 | $jobObj=new JobObject('MyJob', '\Kcloze\Jobs\Jobs\MyJob', 'test1', ['kcloze', time()], $jobExtras=[]); 20 | 21 | $str=Serialize::serialize($jobObj, 'php'); 22 | 23 | $obj=Serialize::unSerialize($str, 'php'); 24 | var_dump($obj); 25 | 26 | $str=Serialize::serialize($jobObj, 'json'); 27 | $obj=Serialize::unSerialize($str, 'json'); 28 | var_dump($obj); 29 | 30 | $str2='{"uuid":"MyJob5b10cd256b53a.1527827749.4396","topic":"MyJob","jobClass":"\\Kcloze\\Jobs\\Jobs\\MyJob","jobMethod":"test1","jobParams":["kcloze",1527827749],"jobExtras":{"delay":84000,"priority":1}}'; 31 | $obj =Serialize::unSerialize($str, 'json'); 32 | var_dump($obj); 33 | --------------------------------------------------------------------------------