├── .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 | 
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 |
--------------------------------------------------------------------------------