├── .gitignore ├── src ├── Exception │ └── ProcessException.php ├── JobAbstract.php ├── Contract │ ├── QueueInterface.php │ ├── WorkerStartInterface.php │ ├── WorkerStopInterface.php │ └── ProcessInterface.php ├── ProcessEvent.php ├── Swoole │ ├── WorkerStopListener.php │ └── WorkerStartListener.php ├── SwooleEvent.php ├── Listener │ ├── AfterProcessListener.php │ ├── WorkerStopListener.php │ ├── BeforeProcessListener.php │ └── AddProcessListener.php ├── Annotation │ ├── Parser │ │ └── JobParser.php │ └── Mapping │ │ └── Job.php ├── Context │ ├── WorkerStopContext.php │ └── ProcessContext.php ├── Manager │ ├── QueueManager.php │ ├── JobsManager.php │ └── AgentManager.php ├── AutoLoader.php ├── QueueAbstract.php ├── Command │ └── QueueCommand.php ├── Process.php └── QueuePool.php ├── composer.json ├── phpunit.xml ├── test └── bootstrap.php ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .buildpath 2 | .settings/ 3 | .project 4 | *.patch 5 | .idea/ 6 | .git/ 7 | runtime/ 8 | vendor/ 9 | temp/ 10 | *.lock 11 | .phpintel/ 12 | .env 13 | .DS_Store -------------------------------------------------------------------------------- /src/Exception/ProcessException.php: -------------------------------------------------------------------------------- 1 | WorkerStartInterface::class, 31 | self::WORKER_STOP => WorkerStopInterface::class 32 | ]; 33 | } 34 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./test/unit 15 | 16 | 17 | 18 | 19 | ./src/* 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Listener/AfterProcessListener.php: -------------------------------------------------------------------------------- 1 | className); 32 | 33 | return [$this->className, $this->className, Bean::SINGLETON, '']; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Context/WorkerStopContext.php: -------------------------------------------------------------------------------- 1 | pool = $pool; 44 | $self->workerId = $workerId; 45 | 46 | return $self; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Listener/WorkerStopListener.php: -------------------------------------------------------------------------------- 1 | getParams(); 35 | 36 | CLog::info("worker(%d) 停止中。。。", $workerId); 37 | \Swoole\Event::wait(); 38 | CLog::info("worker(%d) 已经停止", $workerId); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Listener/BeforeProcessListener.php: -------------------------------------------------------------------------------- 1 | getParams(); 32 | 33 | $context = ProcessContext::new($pool, $workerId); 34 | if (Log::getLogger()->isEnable()) { 35 | $data = [ 36 | 'event' => 'swoft.queue.worker.start', 37 | 'uri' => '', 38 | 'requestTime' => microtime(true), 39 | ]; 40 | $context->setMulti($data); 41 | } 42 | 43 | Context::set($context); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Annotation/Mapping/Job.php: -------------------------------------------------------------------------------- 1 | name = $values['name']; 44 | } 45 | 46 | if (isset($values['queue'])) { 47 | $this->queue = $values['queue']; 48 | } 49 | } 50 | 51 | /** 52 | * @return string 53 | */ 54 | public function getName(): string 55 | { 56 | return $this->name; 57 | } 58 | 59 | /** 60 | * @return string 61 | */ 62 | public function getQueue(): string 63 | { 64 | return $this->queue; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Context/ProcessContext.php: -------------------------------------------------------------------------------- 1 | pool = $pool; 43 | $self->workerId = $workerId; 44 | 45 | return $self; 46 | } 47 | 48 | /** 49 | * @return Pool 50 | */ 51 | public function getPool(): Pool 52 | { 53 | return $this->pool; 54 | } 55 | 56 | /** 57 | * @return int 58 | */ 59 | public function getWorkerId(): int 60 | { 61 | return $this->workerId; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Manager/QueueManager.php: -------------------------------------------------------------------------------- 1 | getQueue($workerId); 31 | 32 | /** @var QueueAbstract $queueManager */ 33 | $queueManager = BeanFactory::getBean($queue['class']); 34 | $queueManager->setQueueKey($queue['queue_key'] ?? 1); 35 | $queueManager->setCoroutineNum($queue["coroutine_num"]??10); 36 | 37 | PhpHelper::call([$queueManager, 'run'], $pool, $workerId); 38 | } catch (\Throwable $e) { 39 | Error::log( 40 | sprintf('启动队列失败(%s %s %d)!', $e->getMessage(), $e->getFile(), $e->getLine()) 41 | ); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/bootstrap.php: -------------------------------------------------------------------------------- 1 | $dir) { 19 | $loader->addPsr4($prefix, $componentDir . '/' . $dir); 20 | } 21 | 22 | // application's vendor 23 | } elseif (file_exists(dirname(__DIR__, 5) . '/autoload.php')) { 24 | /** @var ClassLoader $loader */ 25 | $loader = require dirname(__DIR__, 5) . '/autoload.php'; 26 | 27 | // need load testing psr4 config map 28 | $composerData = json_decode(file_get_contents($componentJson), true); 29 | 30 | foreach ($composerData['autoload-dev']['psr-4'] as $prefix => $dir) { 31 | $loader->addPsr4($prefix, $componentDir . '/' . $dir); 32 | } 33 | } else { 34 | exit('Please run "composer install" to install the dependencies' . PHP_EOL); 35 | } 36 | 37 | $application = new TestApplication([ 38 | 'basePath' => __DIR__ 39 | ]); 40 | $application->setBeanFile(__DIR__ . '/testing/bean.php'); 41 | $application->run(); 42 | -------------------------------------------------------------------------------- /src/AutoLoader.php: -------------------------------------------------------------------------------- 1 | __DIR__, 33 | ]; 34 | } 35 | 36 | /** 37 | * @return array 38 | */ 39 | public function beans(): array 40 | { 41 | return [ 42 | 'queueServer' => [ 43 | 'class' => QueuePool::class, 44 | 'on' => [ 45 | SwooleEvent::WORKER_START => bean(WorkerStartListener::class), 46 | SwooleEvent::WORKER_STOP => bean(WorkerStopListener::class) 47 | ], 48 | 'queue' => [ 49 | // 'log' => [ 50 | // 'class' => JobsManager::class, 51 | // 'worker_num' => 4, 52 | // 'coroutine_num' => 1, 53 | // 'queue_key' => 1, 54 | // 'redis_key' => "queue", 55 | // ], 56 | ], 57 | ] 58 | ]; 59 | } 60 | 61 | /** 62 | * @return array 63 | */ 64 | public function metadata(): array 65 | { 66 | return []; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swoft Process 2 | 3 | 基于redis的队列组件 4 | 5 | 6 | ## 数据流程 7 | ![image](https://github.com/ctfang/images/blob/master/queue/QQ%E5%9B%BE%E7%89%8720191103225258.png) 8 | 9 | ## Install 10 | 11 | - composer command 12 | 13 | ```bash 14 | composer require ctfang/swoft-queue 15 | ``` 16 | 17 | ## 配置,在`bean.php`新增 18 | ```bash 19 | 'queueServer' => [ 20 | 'class' => QueuePool::class, 21 | 'queue' => [ 22 | 'test' => [ // test 队列名称 23 | 'class' => JobsManager::class,// job分发类 24 | 'worker_num' => 4,// 队列开启进程数量,设置比cpu数量多一两个就好了 25 | 'coroutine_num' => 1,// 每个进程内同时处理多少个job 26 | 'queue_key' => 1,// swoole内置功能queue_key,int类型 27 | 'redis_key' => "queue",// redis key 28 | ], 29 | ], 30 | ] 31 | ``` 32 | 33 | 34 | ## 启动 35 | ```bash 36 | php bin/swoft queue:start 37 | ``` 38 | ## Job 处理工作 39 | job是处理任务的最小单位,job是挂靠在queue上的,每个queue可以挂靠很多Job 40 | 41 | ```php 42 | "这里传入Job的内容"]; 82 | $push = [$queue,$job,$msg]; 83 | // 'queue' 是配置对应的 redis_key 值 84 | Redis::lPush('queue',json_encode($push)); 85 | ``` 86 | 87 | ## LICENSE 88 | 89 | The Component is open-sourced software licensed under the [Apache license](LICENSE). 90 | -------------------------------------------------------------------------------- /src/Manager/JobsManager.php: -------------------------------------------------------------------------------- 1 | getQueue()][$jobAnnotation->getName()] = $className; 42 | } 43 | 44 | /** 45 | * 运行初始化 46 | */ 47 | protected function start(): void 48 | { 49 | $queue = QueuePool::$processPool->getQueue($this->workerId); 50 | $arr = QueuePool::$processPool->queueKeyBindQueueName[$queue['queue_key']]; 51 | 52 | foreach ($arr as $queueName=>$infoArr){ 53 | // 只实例化当前队列下的JOB 54 | if (isset(self::$jobs[$queueName])) { 55 | foreach (self::$jobs[$queueName] as $jobName=>$className) { 56 | $this->jobHandle[$queueName][$jobName] = BeanFactory::getBean($className); 57 | } 58 | } 59 | } 60 | } 61 | 62 | /** 63 | * 队列信息处理 64 | * 65 | * @param string $frame 66 | * @throws ProcessException 67 | */ 68 | protected function handle($frame): void 69 | { 70 | $msg = json_decode($frame, true); 71 | $queue = $msg[0]; 72 | $job = $msg[1]; 73 | 74 | if (!isset($this->jobHandle[$queue][$job])) { 75 | throw new ProcessException(sprintf('找不到Job处理,queue = %s;job = %s', $queue,$job)); 76 | } 77 | 78 | PhpHelper::call([$this->jobHandle[$queue][$job], 'handle'], $msg[2]); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Swoole/WorkerStartListener.php: -------------------------------------------------------------------------------- 1 | setSignal($pool,$workerId); 50 | // Init 51 | QueuePool::$processPool->initProcessPool($pool); 52 | 53 | // Before 54 | Swoft::trigger(ProcessEvent::BEFORE_PROCESS, $this, $pool, $workerId); 55 | 56 | if ( $workerId==0 ){ 57 | $this->agentManager->run($pool, $workerId); 58 | }else{ 59 | $this->queueManager->run($pool, $workerId); 60 | } 61 | 62 | // After 63 | Swoft::trigger(ProcessEvent::BEFORE_PROCESS, $this, $pool, $workerId); 64 | } 65 | 66 | 67 | /** 68 | * 设置信号 69 | * 70 | * @param Pool $pool 71 | * @param int $workerId 72 | */ 73 | private function setSignal(Pool $pool, int $workerId) 74 | { 75 | \Swoole\Process::signal(SIGINT, function () use ($pool, $workerId) { 76 | Swoft::trigger(SwooleEvent::WORKER_STOP, null, $pool, $workerId); 77 | $pool->getProcess($workerId)->exit(1); 78 | }); 79 | 80 | \Swoole\Process::signal(SIGTERM, function () use ($pool, $workerId) { 81 | Swoft::trigger(SwooleEvent::WORKER_STOP, null, $pool, $workerId); 82 | $pool->getProcess($workerId)->exit(1); 83 | }); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Manager/AgentManager.php: -------------------------------------------------------------------------------- 1 | queueKeyBindQueueName; 39 | $lastArr = array_pop($queueKeyBindQueueName); 40 | 41 | foreach ($queueKeyBindQueueName as $infoArr) { 42 | 43 | $this->toPop($pool, $infoArr); 44 | } 45 | 46 | $this->toPop($pool, $lastArr); 47 | } 48 | 49 | /** 50 | * @param Pool $pool 51 | * @param array $infoArr 52 | * @throws \Swoft\Redis\Exception\RedisException 53 | */ 54 | private function toPop(Pool $pool, array $infoArr) 55 | { 56 | $queueKey = 1; 57 | $redisKeys = []; 58 | foreach ($infoArr as $info) { 59 | $redisKeys[] = $info['redis_key'] ?? "queue"; 60 | $queueKey = $info['queue_key'] ?? 1; 61 | } 62 | 63 | /** @var Process $queueManagerProcess */ 64 | $queueManagerProcess = $pool->getProcess($this->getWorkerIdForQueueKey($queueKey)); 65 | $queueManagerProcess->useQueue($queueKey, 2); 66 | 67 | $redis = Redis::connection(); 68 | ini_set('default_socket_timeout', -1); 69 | while (QueuePool::$running) { 70 | $arr = $queueManagerProcess->statQueue(); 71 | $queueNum = $arr['queue_num']; 72 | 73 | if ($queueNum < self::$queueLimit) { 74 | for ($i = $queueNum; $i <= self::$queueLimit; $i++) { 75 | $msg = $redis->brPop($redisKeys, 0); 76 | $queueManagerProcess->push($msg[1]); 77 | } 78 | } 79 | Coroutine::sleep(0.5); 80 | } 81 | } 82 | 83 | /** 84 | * 根据queueKey获取对应的workerId 85 | * 86 | * @param string $queueKey 87 | * @return int 88 | */ 89 | private function getWorkerIdForQueueKey(string $queueKey): int 90 | { 91 | return QueuePool::$processPool->queueWorkerId[$queueKey]; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Listener/AddProcessListener.php: -------------------------------------------------------------------------------- 1 | getTarget(); 40 | 41 | $this->addProcess($server); 42 | } 43 | 44 | /** 45 | * Add process 46 | * 47 | * @param Server $server 48 | * 49 | * @throws ServerException 50 | */ 51 | private function addProcess(Server $server): void 52 | { 53 | $process = $server->getProcess(); 54 | if (empty($process)) { 55 | return; 56 | } 57 | 58 | foreach ($process as $name => $userProcess) { 59 | if (!$userProcess instanceof UserProcessInterface) { 60 | throw new ServerException('Server add process must be instanceof UserProcessInterface!'); 61 | } 62 | 63 | $callback = [$userProcess, 'run']; 64 | $stdinOut = $userProcess->isStdinOut(); 65 | $pipeType = $userProcess->getPipeType(); 66 | $coroutine = $userProcess->isCoroutine(); 67 | 68 | $function = function (SwooleProcess $process) use ($callback, $server, $name) { 69 | $process = Process::new($process); 70 | 71 | // Before 72 | Swoft::trigger(ProcessEvent::BEFORE_USER_PROCESS, null, $server, $process, $name); 73 | 74 | try {// Run 75 | PhpHelper::call($callback, $process); 76 | } catch (Throwable $e) { 77 | Error::log('User process fail(%s %s %d)!', $e->getFile(), $e->getMessage(), $e->getLine()); 78 | } 79 | 80 | // After 81 | Swoft::trigger(ProcessEvent::AFTER_USER_PROCESS); 82 | }; 83 | 84 | $process = new SwooleProcess($function, $stdinOut, $pipeType, $coroutine); 85 | $server->getSwooleServer()->addProcess($process); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/QueueAbstract.php: -------------------------------------------------------------------------------- 1 | queueKey; 65 | } 66 | 67 | /** 68 | * @param int $key 69 | */ 70 | public function setQueueKey(int $key) 71 | { 72 | $this->queueKey = $key; 73 | } 74 | 75 | /** 76 | * @return int 77 | */ 78 | public function getCoroutineNum(): int 79 | { 80 | return $this->coroutineNum; 81 | } 82 | 83 | 84 | /** 85 | * 设置初始化多少个协程,同时处理多少个Job 86 | * 87 | * @param int $num 88 | */ 89 | public function setCoroutineNum(int $num) 90 | { 91 | $this->coroutineNum = $num; 92 | } 93 | 94 | /** 95 | * @param int $workerId 96 | */ 97 | protected function setWorkerId(int $workerId) 98 | { 99 | $this->workerId = $workerId; 100 | } 101 | 102 | /** 103 | * @param $pool 104 | */ 105 | protected function setPool(Pool $pool) 106 | { 107 | $this->pool = $pool; 108 | } 109 | 110 | /** 111 | * @param Pool $pool 112 | * @param int $workerId 113 | */ 114 | public function run(Pool $pool, int $workerId): void 115 | { 116 | $this->chan = new chan; 117 | 118 | $this->setWorkerId($workerId); 119 | $this->setPool($pool); 120 | 121 | $this->start(); 122 | 123 | $process = $pool->getProcess($workerId); 124 | $process->useQueue($this->getQueueKey(), $this->mod); 125 | 126 | // 设置了同时处理多个任务 127 | for ($i = 1; $i < $this->getCoroutineNum(); $i++) { 128 | sgo(function () use ($process) { 129 | $this->toHandle($process); 130 | }); 131 | } 132 | // 最后一个 133 | $this->toHandle($process); 134 | } 135 | 136 | /** 137 | * @param $process 138 | */ 139 | private function toHandle($process): void 140 | { 141 | $this->count++; 142 | while (QueuePool::$running) { 143 | $msg = $process->pop(); 144 | $this->handle($msg); 145 | } 146 | // 退出一个计数 147 | $this->chan->push(true); 148 | } 149 | 150 | /** 151 | * 等待所有Job退出后才返回 152 | * 153 | * @return bool 154 | */ 155 | public function wait(): bool 156 | { 157 | while ($this->count--) { 158 | $this->chan->pop(); 159 | } 160 | return true; 161 | } 162 | 163 | /** 164 | * 运行初始化 165 | */ 166 | abstract protected function start(): void; 167 | 168 | /** 169 | * 队列信息处理 170 | * 171 | * @param $frame 172 | */ 173 | abstract protected function handle($frame): void; 174 | } 175 | -------------------------------------------------------------------------------- /src/Command/QueueCommand.php: -------------------------------------------------------------------------------- 1 | createServer(); 38 | 39 | // Check if it has started 40 | if ($server->isRunning()) { 41 | $masterPid = $server->getPid(); 42 | output()->writeln("The Process pool have been running!(PID: {$masterPid})"); 43 | return; 44 | } 45 | 46 | // Daemon 47 | $asDaemon = input()->getSameOpt(['d', 'daemon'], false); 48 | if ($asDaemon) { 49 | $server->setDaemonize(); 50 | } 51 | 52 | $server->start(); 53 | } 54 | 55 | /** 56 | * @CommandMapping(desc="restart the process pool") 57 | * 58 | * @throws ProcessException 59 | */ 60 | public function restart(): void 61 | { 62 | $server = $this->createServer(); 63 | 64 | // Check if it has started 65 | if ($server->isRunning()) { 66 | $success = $server->stop(); 67 | if (!$success) { 68 | output()->error('Stop the old process pool failed!'); 69 | return; 70 | } 71 | } 72 | 73 | output()->writef('Process pool restart success !'); 74 | 75 | $server->setDaemonize(); 76 | $server->start(); 77 | } 78 | 79 | /** 80 | * @CommandMapping(desc="reload the process pool's worker") 81 | */ 82 | public function reload(): void 83 | { 84 | $server = $this->createServer(); 85 | $script = input()->getScript(); 86 | 87 | // Check if it has started 88 | if (!$server->isRunning()) { 89 | output()->writeln('The Process pool is not running! cannot reload'); 90 | return; 91 | } 92 | 93 | output()->writef('Server %s is reloading', $script); 94 | 95 | if (!$server->reload()) { 96 | Show::error('The process pool worker process reload fail!'); 97 | return; 98 | } 99 | 100 | output()->writef('Process pool %s reload success', $script); 101 | } 102 | 103 | /** 104 | * @CommandMapping(desc="stop the process pool") 105 | */ 106 | public function stop(): void 107 | { 108 | $server = $this->createServer(); 109 | 110 | // Check if it has started 111 | if (!$server->isRunning()) { 112 | output()->writeln('The Process pool is not running! cannot stop.'); 113 | return; 114 | } 115 | 116 | // Do stopping. 117 | $server->stop(); 118 | } 119 | 120 | /** 121 | * @return QueuePool 122 | */ 123 | private function createServer(): QueuePool 124 | { 125 | $script = input()->getScript(); 126 | $command = $this->getFullCommand(); 127 | 128 | /** @var QueuePool $processPool */ 129 | $processPool = bean('queueServer'); 130 | $processPool->setScriptFile(Swoft::app()->getPath($script)); 131 | $processPool->setFullCommand($command); 132 | 133 | return $processPool; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/Process.php: -------------------------------------------------------------------------------- 1 | process = $process; 34 | return $self; 35 | } 36 | 37 | /** 38 | * Process constructor. 39 | * 40 | * @param callable $callback 41 | * @param bool $inout 42 | * @param int $pipeType 43 | * @param bool $coroutine 44 | * @param SwooleProcess|null $process 45 | * 46 | * @throws ProcessException 47 | */ 48 | public function __construct( 49 | callable $callback = null, 50 | bool $inout = false, 51 | int $pipeType = 2, 52 | bool $coroutine = true, 53 | SwooleProcess $process = null 54 | ) { 55 | if ($process) { 56 | $this->process = $process; 57 | return; 58 | } 59 | 60 | if (empty($callback)) { 61 | throw new ProcessException('Process callback must be not empty!'); 62 | } 63 | 64 | $this->process = new SwooleProcess($callback, $inout, $pipeType, $coroutine); 65 | } 66 | 67 | /** 68 | * @return int 69 | * @throws ProcessException 70 | */ 71 | public function start(): int 72 | { 73 | $result = $this->process->start(); 74 | if ($result === false) { 75 | throw new ProcessException('Process start fail!'); 76 | } 77 | 78 | return $result; 79 | } 80 | 81 | /** 82 | * @param string $name 83 | */ 84 | public function name(string $name): void 85 | { 86 | $this->process->name($name); 87 | } 88 | 89 | /** 90 | * @param string $shell 91 | * @param array $args 92 | */ 93 | public function exec(string $shell, array $args): void 94 | { 95 | $this->process->exec($shell, $args); 96 | } 97 | 98 | /** 99 | * @param string $data 100 | * 101 | * @return int 102 | * @throws ProcessException 103 | */ 104 | public function write(string $data): int 105 | { 106 | $result = $this->process->write($data); 107 | if ($result !== false) { 108 | return (int)$result; 109 | } 110 | 111 | $error = $this->getError(); 112 | throw new ProcessException(sprintf('Process write fail!(%s)', $error)); 113 | } 114 | 115 | /** 116 | * @param int $bufferSize 117 | * 118 | * @return string 119 | * @throws ProcessException 120 | */ 121 | public function read(int $bufferSize = 8192): string 122 | { 123 | $result = $this->process->read($bufferSize); 124 | if ($result === false) { 125 | throw new ProcessException('Process read file'); 126 | } 127 | 128 | return (string)$result; 129 | } 130 | 131 | /** 132 | * @param float $seconds 133 | * 134 | * @return bool 135 | */ 136 | public function setTimeout(float $seconds): bool 137 | { 138 | return (bool)$this->process->setTimeout($seconds); 139 | } 140 | 141 | /** 142 | * @param bool $blocking 143 | * 144 | * @return bool 145 | */ 146 | public function setBlocking(bool $blocking = true): bool 147 | { 148 | return (bool)$this->process->setBlocking($blocking); 149 | } 150 | 151 | /** 152 | * @param int $msgkey 153 | * @param int $mode 154 | * @param int $capacity 155 | * 156 | * @return bool 157 | */ 158 | public function useQueue(int $msgkey = 0, int $mode = 2, int $capacity = 8192): bool 159 | { 160 | return $this->process->useQueue($msgkey, $mode, $capacity); 161 | } 162 | 163 | /** 164 | * @return array 165 | */ 166 | public function statQueue(): array 167 | { 168 | return $this->process->statQueue(); 169 | } 170 | 171 | /** 172 | * @return bool 173 | */ 174 | public function freeQueue(): bool 175 | { 176 | return (bool)$this->process->freeQueue(); 177 | } 178 | 179 | /** 180 | * @return Socket 181 | */ 182 | public function exportSocket(): Socket 183 | { 184 | return $this->process->exportSocket(); 185 | } 186 | 187 | /** 188 | * @param string $data 189 | * 190 | * @return bool 191 | */ 192 | public function push(string $data): bool 193 | { 194 | return $this->process->push($data); 195 | } 196 | 197 | /** 198 | * @param int $maxSize 199 | * 200 | * @return string 201 | * @throws ProcessException 202 | */ 203 | public function pop(int $maxSize = 8192): string 204 | { 205 | $result = $this->process->pop($maxSize); 206 | if ($result !== false) { 207 | return (string)$result; 208 | } 209 | 210 | $error = $this->getError(); 211 | throw new ProcessException($error); 212 | } 213 | 214 | /** 215 | * @param int $which 216 | * 217 | * @return bool 218 | */ 219 | public function close(int $which = 0): bool 220 | { 221 | return (bool)$this->process->close($which); 222 | } 223 | 224 | /** 225 | * @param int $status 226 | * 227 | * @return int 228 | */ 229 | public function exit(int $status = 0): int 230 | { 231 | return (int)$this->process->exit($status); 232 | } 233 | 234 | /** 235 | * @param int $pid 236 | * @param int $signo 237 | * 238 | * @return bool 239 | */ 240 | public static function kill(int $pid, $signo = 15): bool 241 | { 242 | return (bool)SwooleProcess::kill($pid, $signo); 243 | } 244 | 245 | /** 246 | * @param bool $blocking 247 | * 248 | * @return array 249 | * @throws ProcessException 250 | */ 251 | public static function wait(bool $blocking = true): array 252 | { 253 | $result = SwooleProcess::wait($blocking); 254 | if ($result !== false) { 255 | return (array)$result; 256 | } 257 | 258 | throw new ProcessException(sprintf('Process wait fail!')); 259 | } 260 | 261 | /** 262 | * @param bool $nochDir 263 | * @param bool $noClose 264 | * 265 | * @return bool 266 | */ 267 | public static function daemon(bool $nochDir = false, bool $noClose = false): bool 268 | { 269 | return (bool)SwooleProcess::daemon($nochDir, $noClose); 270 | } 271 | 272 | /** 273 | * @param int $signo 274 | * @param callable|null $callback 275 | * 276 | * @return bool 277 | */ 278 | public static function signal(int $signo, callable $callback = null): bool 279 | { 280 | return (bool)SwooleProcess::signal($signo, $callback); 281 | } 282 | 283 | /** 284 | * @param int $intervalUsec 285 | * @param int $type 286 | * 287 | * @return bool 288 | */ 289 | public static function alarm(int $intervalUsec, int $type = 0): bool 290 | { 291 | return (bool)SwooleProcess::alarm($intervalUsec, $type); 292 | } 293 | 294 | /** 295 | * @param array $cpuSet 296 | * 297 | * @return bool 298 | */ 299 | public static function setAffinity(array $cpuSet): bool 300 | { 301 | return (bool)SwooleProcess::setAffinity($cpuSet); 302 | } 303 | 304 | /** 305 | * @return string 306 | */ 307 | private function getError(): string 308 | { 309 | $errno = swoole_errno(); 310 | return (string)swoole_strerror($errno); 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /src/QueuePool.php: -------------------------------------------------------------------------------- 1 | xxxListener 72 | * ] 73 | */ 74 | private $on = []; 75 | 76 | /** 77 | * @var string 78 | */ 79 | private $pidFile = '@runtime/swoft-queue.pid'; 80 | 81 | /** 82 | * @var string 83 | */ 84 | private $pidName = 'swoft-queue'; 85 | 86 | /** 87 | * @var string 88 | */ 89 | private $scriptFile = ''; 90 | 91 | /** 92 | * @var string 93 | */ 94 | private $fullCommand = ''; 95 | 96 | /** 97 | * @var int 98 | */ 99 | private $masterPid = 0; 100 | 101 | /** 102 | * @var string 103 | */ 104 | private $commandFile = '@runtime/swoft-queue.command'; 105 | 106 | /** 107 | * bean配置赋值 108 | * 109 | * @var array 110 | */ 111 | private $queue = []; 112 | 113 | /** 114 | * @var array 115 | */ 116 | private $workerQueue = []; 117 | 118 | 119 | /** 120 | * queueKey 任意映射一个 workerId 121 | * @var array 122 | */ 123 | public $queueWorkerId = []; 124 | 125 | /** 126 | * queueKey 映射 queueName 127 | * 128 | * @var array 129 | */ 130 | public $queueKeyBindQueueName = []; 131 | 132 | /** 133 | * Start process pool 134 | * 135 | * @throws ProcessException 136 | */ 137 | public function start(): void 138 | { 139 | // 统计进程数量 140 | $this->workerNum = 0; 141 | foreach ($this->queue as $key => $queueInfo) { 142 | $num = (int)($queueInfo['worker_num'] ?? 1); 143 | $queueKey = (int)($queueInfo['queue_key'] ?? 1); 144 | $this->workerNum += $num; 145 | 146 | // 分配workerId=>queueName 147 | for ($workerId = $this->workerNum; $workerId > ($this->workerNum - $num); $workerId--) { 148 | $this->workerQueue[$workerId] = $key; 149 | $this->queueWorkerId[$queueKey] = $workerId; 150 | } 151 | 152 | // 映射建立 153 | $this->queueKeyBindQueueName[$queueKey][$key] = $queueInfo; 154 | } 155 | 156 | // 检查redis key 是否设置正常 157 | $checkKeys = []; 158 | foreach ($this->queueKeyBindQueueName as $queueKey => $arr) { 159 | foreach ($arr as $queue => $info) { 160 | $redisKey = $info['redis_key'] ?? "queue"; 161 | if (isset($checkKeys[$redisKey])) { 162 | if ($checkKeys[$redisKey] != $queueKey) { 163 | throw new ProcessException(sprintf('一个redis key 只能对应一个queue_key; = %d', $queueKey)); 164 | } 165 | } 166 | $checkKeys[$redisKey] = $queueKey; 167 | } 168 | } 169 | 170 | // 设置 Pool 171 | $this->pool = new Pool($this->workerNum + 1, $this->ipcType, $this->msgQueueKey, $this->coroutine); 172 | foreach ($this->on as $name => $listener) { 173 | $listenerInterface = SwooleEvent::LISTENER_MAPPING[$name] ?? ''; 174 | if (empty($listenerInterface)) { 175 | throw new ProcessException(sprintf('Process listener(%s) is not exist!', $name)); 176 | } 177 | 178 | if (!$listener instanceof $listenerInterface) { 179 | throw new ProcessException(sprintf('Listener(%s) must be instanceof %s', $name, $listenerInterface)); 180 | } 181 | 182 | $listenerMethod = sprintf('on%s', ucfirst($name)); 183 | $this->pool->on($name, [$listener, $listenerMethod]); 184 | } 185 | 186 | // Set process name 187 | $this->setProcessName(); 188 | 189 | self::$processPool = $this; 190 | 191 | $this->pool->start(); 192 | } 193 | 194 | /** 195 | * @return bool 196 | */ 197 | public function reload(): bool 198 | { 199 | if (($pid = $this->masterPid) < 1) { 200 | return false; 201 | } 202 | 203 | // SIGUSR1 to reload 204 | return ServerHelper::sendSignal($pid, 10); 205 | } 206 | 207 | /** 208 | * @return bool 209 | */ 210 | public function stop(): bool 211 | { 212 | $pid = $this->getPid(); 213 | if ($pid < 1) { 214 | return false; 215 | } 216 | 217 | // SIGTERM = 15 218 | if (ServerHelper::killAndWait($pid, 15, $this->pidName)) { 219 | $rmPidOk = ServerHelper::removePidFile(alias($this->pidFile)); 220 | $rmCmdOk = ServerHelper::removePidFile(alias($this->commandFile)); 221 | 222 | return $rmPidOk && $rmCmdOk; 223 | } 224 | 225 | return false; 226 | } 227 | 228 | /** 229 | * Quick restart 230 | * @throws SwoftException 231 | */ 232 | public function restart(): void 233 | { 234 | if ($this->isRunning()) { 235 | // Restart command 236 | $command = Co::readFile(alias($this->commandFile)); 237 | 238 | // Stop server 239 | $this->stop(); 240 | 241 | // Exe restart shell 242 | Coroutine::exec($command); 243 | 244 | CLog::info('Restart success(%s)!', $command); 245 | } 246 | } 247 | 248 | /** 249 | * @param Pool $pool 250 | */ 251 | public function initProcessPool(Pool $pool): void 252 | { 253 | // Set process 254 | Sys::setProcessTitle(sprintf('%s-%s', $this->pidName, 'worker')); 255 | 256 | // Save PID to file 257 | $pidFile = alias($this->pidFile); 258 | Dir::make(dirname($pidFile)); 259 | file_put_contents($pidFile, $pool->master_pid); 260 | 261 | // Save pull command to file 262 | $commandFile = alias($this->commandFile); 263 | Dir::make(dirname($commandFile)); 264 | file_put_contents($commandFile, $this->fullCommand); 265 | } 266 | 267 | /** 268 | * Check if process pool is running 269 | * 270 | * @return bool 271 | */ 272 | public function isRunning(): bool 273 | { 274 | $pidFile = alias($this->pidFile); 275 | 276 | // Is pid file exist ? 277 | if (file_exists($pidFile)) { 278 | // Get pid file content and parse the content 279 | $masterPid = file_get_contents($pidFile); 280 | 281 | // Format type 282 | $masterPid = (int)$masterPid; 283 | 284 | $this->masterPid = $masterPid; 285 | 286 | // Notice: skip pid 1, resolve start server on docker. 287 | return $masterPid > 1 && Process::kill($masterPid, 0); 288 | } 289 | 290 | return false; 291 | } 292 | 293 | /** 294 | * Set server, run server on the background 295 | * 296 | * @param bool $yes 297 | * 298 | * @return $this 299 | */ 300 | public function setDaemonize(bool $yes = true): self 301 | { 302 | if ($yes) { 303 | Process::daemon(true, false); 304 | } 305 | 306 | return $this; 307 | } 308 | 309 | /** 310 | * @param string $scriptFile 311 | */ 312 | public function setScriptFile(string $scriptFile): void 313 | { 314 | $this->scriptFile = $scriptFile; 315 | } 316 | 317 | /** 318 | * @return int 319 | */ 320 | public function getPid(): int 321 | { 322 | return $this->masterPid; 323 | } 324 | 325 | /** 326 | * @param string $fullCommand 327 | */ 328 | public function setFullCommand(string $fullCommand): void 329 | { 330 | $this->fullCommand = $fullCommand; 331 | } 332 | 333 | /** 334 | * @return string 335 | */ 336 | public function getPidName(): string 337 | { 338 | return $this->pidName; 339 | } 340 | 341 | /** 342 | * @return string 343 | */ 344 | public function getPidFile(): string 345 | { 346 | return $this->pidFile; 347 | } 348 | 349 | /** 350 | * Set process name 351 | */ 352 | private function setProcessName(): void 353 | { 354 | Sys::setProcessTitle(sprintf('%s-%s', $this->pidName, 'master')); 355 | } 356 | 357 | /** 358 | * @param int $workerId 359 | * @return array 360 | */ 361 | public function getQueue(int $workerId): array 362 | { 363 | $key = $this->workerQueue[$workerId]; 364 | return $this->queue[$key]; 365 | } 366 | } 367 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------