├── .gitignore ├── LICENSE.txt ├── README.md ├── composer.json └── src ├── Access └── Endpoint.php ├── Connection ├── Lookupd.php ├── Nsqd.php ├── Pool.php └── Transport │ ├── HTTP.php │ └── TCP.php ├── Contract ├── Message.php └── Network │ └── Stream.php ├── Exception ├── GenericErrorException.php ├── InvalidLookupdException.php ├── InvalidMessageException.php ├── LookupTopicException.php ├── NetworkSocketException.php ├── NetworkTimeoutException.php ├── PoolMissingSocketException.php ├── UnknownFrameException.php └── UnknownProtocolException.php ├── Logger ├── EchoLogger.php └── Logger.php ├── Message ├── Bag.php └── Message.php ├── Protocol ├── Binary.php ├── Command.php ├── CommandHTTP.php └── Specification.php ├── Queue.php ├── SDK.php └── Utils └── GracefulShutdown.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | composer.lock -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2017] [moyo] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NSQClient 2 | 3 | Yet another PHP client for [NSQ](http://nsq.io) 4 | 5 | ### Installation (via composer) 6 | 7 | ``` 8 | composer require moolex/nsqclient dev-master 9 | ``` 10 | 11 | ### Usage 12 | 13 | #### Publish 14 | 15 | ```php 16 | $topic = 'my_topic'; 17 | $endpoint = new \NSQClient\Access\Endpoint('http://127.0.0.1:4161'); 18 | $message = new \NSQClient\Message\Message('hello world'); 19 | $result = \NSQClient\Queue::publish($endpoint, $topic, $message); 20 | ``` 21 | 22 | #### Publish (deferred) 23 | 24 | ```php 25 | $topic = 'my_topic'; 26 | $endpoint = new \NSQClient\Access\Endpoint('http://127.0.0.1:4161'); 27 | $message = (new \NSQClient\Message\Message('hello world'))->deferred(5); 28 | $result = \NSQClient\Queue::publish($endpoint, $topic, $message); 29 | ``` 30 | 31 | #### Publish (batch) 32 | 33 | ```php 34 | $topic = 'my_topic'; 35 | $endpoint = new \NSQClient\Access\Endpoint('http://127.0.0.1:4161'); 36 | $message = \NSQClient\Message\Bag::generate(['msg data 1', 'msg data 2']); 37 | $result = \NSQClient\Queue::publish($endpoint, $topic, $message); 38 | ``` 39 | 40 | #### Subscribe 41 | 42 | ```php 43 | $topic = 'my_topic'; 44 | $channel = 'my_channel'; 45 | $endpoint = new \NSQClient\Access\Endpoint('http://127.0.0.1:4161'); 46 | \NSQClient\Queue::subscribe($endpoint, $topic, $channel, function (\NSQClient\Contract\Message $message) { 47 | echo 'GOT ', $message->id(), "\n"; 48 | // make done 49 | $message->done(); 50 | // make retry immediately 51 | // $message->retry(); 52 | // make retry delayed in 10 seconds 53 | // $message->delay(10); 54 | }); 55 | ``` 56 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "moolex/nsqclient", 3 | "keywords": ["nsq"], 4 | "license": "MIT", 5 | "require": { 6 | "php": ">=5.6", 7 | "ext-json" : "*", 8 | "ext-curl" : "*", 9 | "ext-bcmath" : "*", 10 | "psr/log": "~1.0", 11 | "react/event-loop": "~0.4" 12 | }, 13 | "suggest": { 14 | "ext-pcntl": "graceful shutdown" 15 | }, 16 | "autoload":{ 17 | "psr-4": { 18 | "NSQClient\\": "src" 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/Access/Endpoint.php: -------------------------------------------------------------------------------- 1 | lookupd = $lookupd; 33 | $this->uniqueID = spl_object_hash($this); 34 | 35 | // checks 36 | $parsed = parse_url($this->lookupd); 37 | if (!isset($parsed['host'])) { 38 | throw new InvalidLookupdException; 39 | } 40 | } 41 | 42 | /** 43 | * @return string 44 | */ 45 | public function getUniqueID() 46 | { 47 | return $this->uniqueID; 48 | } 49 | 50 | /** 51 | * @return string 52 | */ 53 | public function getLookupd() 54 | { 55 | return $this->lookupd; 56 | } 57 | 58 | /** 59 | * @return string 60 | */ 61 | public function getConnType() 62 | { 63 | return PHP_SAPI == 'cli' ? 'tcp' : 'http'; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Connection/Lookupd.php: -------------------------------------------------------------------------------- 1 | getUniqueID()][$topic])) { 37 | return self::$caches[$endpoint->getUniqueID()][$topic]; 38 | } 39 | 40 | $url = $endpoint->getLookupd() . sprintf(self::$queryFormat, $topic); 41 | 42 | list($error, $result) = HTTP::get($url); 43 | 44 | if ($error) { 45 | list($netErrNo, $netErrMsg) = $error; 46 | Logger::ins()->error('Lookupd request failed', ['no' => $netErrNo, 'msg' => $netErrMsg]); 47 | throw new LookupTopicException($netErrMsg, $netErrNo); 48 | } else { 49 | Logger::ins()->debug('Lookupd results got', ['raw' => $result]); 50 | return self::$caches[$endpoint->getUniqueID()][$topic] = self::parseResult($result, $topic); 51 | } 52 | } 53 | 54 | /** 55 | * @param $rawJson 56 | * @param $scopeTopic 57 | * @return array 58 | */ 59 | private static function parseResult($rawJson, $scopeTopic) 60 | { 61 | $result = json_decode($rawJson, true); 62 | 63 | $nodes = []; 64 | 65 | if (isset($result['producers'])) { 66 | foreach ($result['producers'] as $producer) { 67 | $nodes[] = [ 68 | 'topic' => $scopeTopic, 69 | 'host' => $producer['broadcast_address'], 70 | 'ports' => [ 71 | 'tcp' => $producer['tcp_port'], 72 | 'http' => $producer['http_port'] 73 | ] 74 | ]; 75 | } 76 | } 77 | 78 | return $nodes; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Connection/Nsqd.php: -------------------------------------------------------------------------------- 1 | endpoint = $endpoint; 78 | 79 | if ($this->endpoint->getConnType() == 'tcp') { 80 | $this->connTCP = new TCP; 81 | $this->connTCP->setHandshake([$this, 'handshake']); 82 | } 83 | } 84 | 85 | /** 86 | * @param $route 87 | * @return self 88 | */ 89 | public function setRoute($route) 90 | { 91 | $this->host = $route['host']; 92 | $this->portTCP = $route['ports']['tcp']; 93 | $this->portHTTP = $route['ports']['http']; 94 | 95 | if ($this->connTCP) { 96 | $this->connTCP->setTarget($this->host, $this->portTCP); 97 | } 98 | 99 | return $this; 100 | } 101 | 102 | /** 103 | * @param $topic 104 | * @return self 105 | */ 106 | public function setTopic($topic) 107 | { 108 | $this->topic = $topic; 109 | 110 | return $this; 111 | } 112 | 113 | /** 114 | * @param $seconds 115 | * @return self 116 | */ 117 | public function setLifecycle($seconds) 118 | { 119 | $this->lifecycle = $seconds; 120 | 121 | return $this; 122 | } 123 | 124 | /** 125 | * @return self 126 | */ 127 | public function setProducer() 128 | { 129 | if ($this->connTCP) { 130 | $this->connTCP->setRecycling($this->lifecycle); 131 | } 132 | 133 | return $this; 134 | } 135 | 136 | /** 137 | * @param callable $processor 138 | * @return self 139 | */ 140 | public function setConsumer(callable $processor) 141 | { 142 | $this->subProcessor = $processor; 143 | 144 | if ($this->lifecycle) { 145 | $nsqd = $this; 146 | Pool::getEvLoop()->addTimer($this->lifecycle, function () use ($nsqd) { 147 | $nsqd->closing(); 148 | }); 149 | } 150 | 151 | return $this; 152 | } 153 | 154 | /** 155 | * @return int 156 | */ 157 | public function getSockID() 158 | { 159 | return (int)$this->connTCP->socket(); 160 | } 161 | 162 | /** 163 | * @return Stream 164 | */ 165 | public function getSockIns() 166 | { 167 | return $this->connTCP; 168 | } 169 | 170 | /** 171 | * @return bool 172 | */ 173 | public function isConsumer() 174 | { 175 | return ! is_null($this->subProcessor); 176 | } 177 | 178 | /** 179 | * @param Stream $stream 180 | */ 181 | public function handshake(Stream $stream) 182 | { 183 | $stream->write(Command::magic()); 184 | } 185 | 186 | /** 187 | * @param $message 188 | * @return bool 189 | */ 190 | public function publish($message) 191 | { 192 | return 193 | $this->endpoint->getConnType() == 'tcp' 194 | ? $this->publishViaTCP($message) 195 | : $this->publishViaHTTP($message) 196 | ; 197 | } 198 | 199 | /** 200 | * @param $channel 201 | */ 202 | public function subscribe($channel) 203 | { 204 | $this->connTCP->setBlocking(false); 205 | 206 | $evLoop = Pool::getEvLoop(); 207 | 208 | $evLoop->addReadStream($this->connTCP->socket(), function ($socket) { 209 | $this->dispatching(Specification::readFrame(Pool::search($socket))); 210 | }); 211 | 212 | $this->connTCP->write(Command::identify(getmypid(), gethostname(), sprintf('%s/%s', SDK::NAME, SDK::VERSION))); 213 | $this->connTCP->write(Command::subscribe($this->topic, $channel)); 214 | $this->connTCP->write(Command::ready(1)); 215 | 216 | Pool::setEvAttached(); 217 | 218 | Logger::ins()->debug('Consumer is ready', $this->loggingMeta()); 219 | } 220 | 221 | /** 222 | * @param $messageID 223 | */ 224 | public function finish($messageID) 225 | { 226 | Logger::ins()->debug('Make message is finished', $this->loggingMeta(['id' => $messageID])); 227 | $this->connTCP->write(Command::finish($messageID)); 228 | } 229 | 230 | /** 231 | * @param $messageID 232 | * @param $millisecond 233 | */ 234 | public function requeue($messageID, $millisecond) 235 | { 236 | Logger::ins()->debug( 237 | 'Make message is requeued', 238 | $this->loggingMeta(['id' => $messageID, 'delay' => $millisecond]) 239 | ); 240 | $this->connTCP->write(Command::requeue($messageID, $millisecond)); 241 | } 242 | 243 | /** 244 | * subscribe closing 245 | */ 246 | public function closing() 247 | { 248 | Logger::ins()->info('Consumer is closing', $this->loggingMeta()); 249 | $this->connTCP->write(Command::close()); 250 | } 251 | 252 | /** 253 | * process exiting 254 | */ 255 | private function exiting() 256 | { 257 | Logger::ins()->info('Consumer is exiting', $this->loggingMeta()); 258 | $this->connTCP->close(); 259 | Pool::setEvDetached(); 260 | } 261 | 262 | /** 263 | * @param $message 264 | * @return bool 265 | */ 266 | private function publishViaHTTP($message) 267 | { 268 | if ($message instanceof Message) { 269 | list($uri, $data) = CommandHTTP::message($this->topic, $message->data()); 270 | } elseif ($message instanceof MessageBag) { 271 | list($uri, $data) = CommandHTTP::messages($this->topic, $message->export()); 272 | } else { 273 | Logger::ins()->error('Un-expected pub message', $this->loggingMeta(['input' => json_encode($message)])); 274 | throw new InvalidMessageException('Unknowns message object'); 275 | } 276 | 277 | list($error, $result) = HTTP::post(sprintf('http://%s:%d/%s', $this->host, $this->portHTTP, $uri), $data); 278 | 279 | if ($error) { 280 | list($netErrNo, $netErrMsg) = $error; 281 | Logger::ins()->error( 282 | 'HTTP Publish is failed', 283 | $this->loggingMeta(['no' => $netErrNo, 'msg' => $netErrMsg]) 284 | ); 285 | throw new NetworkSocketException($netErrMsg, $netErrNo); 286 | } else { 287 | return $result === 'OK' ? true : false; 288 | } 289 | } 290 | 291 | /** 292 | * @param $message 293 | * @return bool 294 | */ 295 | private function publishViaTCP($message) 296 | { 297 | if ($message instanceof Message) { 298 | $buffer = Command::message($this->topic, $message->data(), $message->deferred()); 299 | } elseif ($message instanceof MessageBag) { 300 | $buffer = Command::messages($this->topic, $message->export()); 301 | } else { 302 | Logger::ins()->error('Un-expected pub message', $this->loggingMeta(['input' => json_encode($message)])); 303 | throw new InvalidMessageException('Unknowns message object'); 304 | } 305 | 306 | $this->connTCP->write($buffer); 307 | 308 | do { 309 | $result = $this->dispatching(Specification::readFrame($this->connTCP)); 310 | } while (is_null($result)); 311 | 312 | return $result; 313 | } 314 | 315 | /** 316 | * @param $frame 317 | * @return bool|null 318 | */ 319 | private function dispatching($frame) 320 | { 321 | switch (true) { 322 | case Specification::frameIsOK($frame): 323 | return true; 324 | break; 325 | case Specification::frameIsMessage($frame): 326 | Logger::ins()->debug( 327 | 'FRAME got is message', 328 | $this->loggingMeta(['id' => $frame['id'], 'data' => $frame['payload']]) 329 | ); 330 | $this->processingMessage( 331 | new Message( 332 | $frame['payload'], 333 | $frame['id'], 334 | $frame['attempts'], 335 | $frame['timestamp'], 336 | $this 337 | ) 338 | ); 339 | return null; 340 | break; 341 | case Specification::frameIsHeartbeat($frame): 342 | Logger::ins()->debug('FRAME got is heartbeat', $this->loggingMeta()); 343 | $this->connTCP->write(Command::nop()); 344 | return null; 345 | break; 346 | case Specification::frameIsError($frame): 347 | Logger::ins()->error('FRAME got is error', $this->loggingMeta(['error' => $frame['error']])); 348 | throw new GenericErrorException($frame['error']); 349 | break; 350 | case Specification::frameIsBroken($frame): 351 | Logger::ins()->warning('FRAME got is broken', $this->loggingMeta(['error' => $frame['error']])); 352 | throw new GenericErrorException($frame['error']); 353 | break; 354 | case Specification::frameIsCloseWait($frame): 355 | Logger::ins()->debug('FRAME got is close-wait', $this->loggingMeta()); 356 | $this->exiting(); 357 | return null; 358 | break; 359 | default: 360 | Logger::ins()->warning('FRAME got is unknowns', $this->loggingMeta()); 361 | throw new UnknownProtocolException('Unknowns protocol data ('.json_encode($frame).')'); 362 | } 363 | } 364 | 365 | /** 366 | * @param Message $message 367 | */ 368 | private function processingMessage(Message $message) 369 | { 370 | try { 371 | call_user_func_array($this->subProcessor, [$message]); 372 | } catch (\Exception $exception) { 373 | // TODO add observer for usr callback 374 | Logger::ins()->critical('Consuming processor has exception', $this->loggingMeta([ 375 | 'cls' => get_class($exception), 376 | 'msg' => $exception->getMessage() 377 | ])); 378 | } 379 | } 380 | 381 | /** 382 | * @param $extra 383 | * @return array 384 | */ 385 | private function loggingMeta($extra = []) 386 | { 387 | return array_merge([ 388 | 'topic' => $this->topic, 389 | 'host' => $this->host, 390 | 'port-tcp' => $this->portTCP 391 | ], $extra); 392 | } 393 | } 394 | -------------------------------------------------------------------------------- /src/Connection/Pool.php: -------------------------------------------------------------------------------- 1 | getSockID() == $expectSockID) { 79 | self::$sockMaps[$nsqd->getSockID()] = $nsqd->getSockIns(); 80 | return $nsqd->getSockIns(); 81 | } 82 | } 83 | 84 | throw new PoolMissingSocketException; 85 | } 86 | 87 | /** 88 | * @return LoopInterface 89 | */ 90 | public static function getEvLoop() 91 | { 92 | if (is_null(self::$evLoops)) { 93 | self::$evLoops = Factory::create(); 94 | GracefulShutdown::init(self::$evLoops); 95 | } 96 | return self::$evLoops; 97 | } 98 | 99 | /** 100 | * New attach by consumer connects 101 | */ 102 | public static function setEvAttached() 103 | { 104 | self::$evAttached ++; 105 | } 106 | 107 | /** 108 | * New detach by consumer closing 109 | */ 110 | public static function setEvDetached() 111 | { 112 | self::$evAttached --; 113 | if (self::$evAttached <= 0) { 114 | Logger::ins()->info('ALL event detached .. perform shutdown'); 115 | self::$evLoops && self::$evLoops->stop(); 116 | } 117 | } 118 | 119 | /** 120 | * @param $factors 121 | * @return string 122 | */ 123 | private static function getInsKey($factors) 124 | { 125 | return implode('$', $factors); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Connection/Transport/HTTP.php: -------------------------------------------------------------------------------- 1 | true, CURLOPT_POSTFIELDS => $data], $extOptions); 49 | } 50 | 51 | /** 52 | * @param $url 53 | * @param $selfOptions 54 | * @param $usrOptions 55 | * @return array 56 | */ 57 | private static function request($url, $selfOptions, $usrOptions) 58 | { 59 | $ch = curl_init(); 60 | 61 | $initOptions = [ 62 | CURLOPT_URL => $url, 63 | CURLOPT_RETURNTRANSFER => true, 64 | CURLOPT_HEADER => false, 65 | CURLOPT_FOLLOWLOCATION => false, 66 | CURLOPT_ENCODING => self::$encoding, 67 | CURLOPT_USERAGENT => self::$agent, 68 | CURLOPT_HTTPHEADER => self::$headers, 69 | CURLOPT_FAILONERROR => true 70 | ]; 71 | 72 | $selfOptions && $initOptions = self::mergeOptions($initOptions, $selfOptions); 73 | $usrOptions && $initOptions = self::mergeOptions($initOptions, $usrOptions); 74 | 75 | curl_setopt_array($ch, $initOptions); 76 | 77 | $result = curl_exec($ch); 78 | 79 | $error = curl_errno($ch) ? [curl_errno($ch), curl_error($ch)] : null; 80 | 81 | curl_close($ch); 82 | 83 | return [$error, $result]; 84 | } 85 | 86 | /** 87 | * @param $base 88 | * @param $custom 89 | * @return mixed 90 | */ 91 | private static function mergeOptions($base, $custom) 92 | { 93 | foreach ($custom as $key => $val) { 94 | $base[$key] = $val; 95 | } 96 | return $base; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Connection/Transport/TCP.php: -------------------------------------------------------------------------------- 1 | host = $host; 79 | $this->port = $port; 80 | } 81 | 82 | /** 83 | * @param $switch 84 | */ 85 | public function setBlocking($switch) 86 | { 87 | $this->blocking = $switch ? true : false; 88 | } 89 | 90 | /** 91 | * @param string $ch 92 | * @param float $time 93 | */ 94 | public function setTimeout($ch = 'rw', $time = 5.0) 95 | { 96 | if ($ch === 'r' || $ch === 'rw') { 97 | $this->readTimeoutSec = floor($time); 98 | $this->readTimeoutUsec = ($time - $this->readTimeoutSec) * 1000000; 99 | } 100 | 101 | if ($ch === 'w' || $ch === 'rw') { 102 | $this->writeTimeoutSec = floor($time); 103 | $this->writeTimeoutUsec = ($time - $this->writeTimeoutSec) * 1000000; 104 | } 105 | } 106 | 107 | /** 108 | * @param $seconds 109 | */ 110 | public function setRecycling($seconds) 111 | { 112 | $this->connRecyclingSec = $seconds; 113 | } 114 | 115 | /** 116 | * @param callable $processor 117 | */ 118 | public function setHandshake(callable $processor) 119 | { 120 | $this->handshake = $processor; 121 | } 122 | 123 | /** 124 | * @return resource 125 | */ 126 | public function socket() 127 | { 128 | if ($this->socket) { 129 | if ($this->connRecyclingSec 130 | && $this->connEstablishedTime 131 | && (time() - $this->connEstablishedTime > $this->connRecyclingSec) 132 | ) { 133 | $this->close(); 134 | } else { 135 | return $this->socket; 136 | } 137 | } 138 | 139 | $netErrNo = $netErrMsg = null; 140 | 141 | $this->socket = fsockopen($this->host, $this->port, $netErrNo, $netErrMsg); 142 | 143 | if ($this->socket === false) { 144 | throw new NetworkSocketException( 145 | "Connecting failed [{$this->host}:{$this->port}] - {$netErrMsg}", 146 | $netErrNo 147 | ); 148 | } else { 149 | $this->connEstablishedTime = time(); 150 | } 151 | 152 | stream_set_blocking($this->socket, $this->blocking ? 1 : 0); 153 | 154 | if (is_callable($this->handshake)) { 155 | call_user_func($this->handshake, $this); 156 | } 157 | 158 | return $this->socket; 159 | } 160 | 161 | /** 162 | * @param $buf 163 | */ 164 | public function write($buf) 165 | { 166 | $null = null; 167 | $socket = $this->socket(); 168 | $writeCh = [$socket]; 169 | 170 | while (strlen($buf) > 0) { 171 | $writable = stream_select($null, $writeCh, $null, $this->writeTimeoutSec, $this->writeTimeoutUsec); 172 | if ($writable > 0) { 173 | $wroteLen = stream_socket_sendto($socket, $buf); 174 | if ($wroteLen === -1 || $wroteLen === false) { 175 | throw new NetworkSocketException("Writing failed [{$this->host}:{$this->port}](1)"); 176 | } 177 | $buf = substr($buf, $wroteLen); 178 | } elseif ($writable === 0) { 179 | throw new NetworkTimeoutException("Writing timeout [{$this->host}:{$this->port}]"); 180 | } else { 181 | throw new NetworkSocketException("Writing failed [{$this->host}:{$this->port}](2)"); 182 | } 183 | } 184 | } 185 | 186 | /** 187 | * @param $len 188 | * @return string 189 | */ 190 | public function read($len) 191 | { 192 | $null = null; 193 | $socket = $this->socket(); 194 | $readCh = [$socket]; 195 | 196 | $remainingLen = $len; 197 | $buffer = ''; 198 | 199 | while (strlen($buffer) < $len) { 200 | $readable = stream_select($readCh, $null, $null, $this->readTimeoutSec, $this->readTimeoutUsec); 201 | if ($readable > 0) { 202 | $recv = stream_socket_recvfrom($socket, $remainingLen); 203 | if ($recv === false) { 204 | throw new NetworkSocketException("Reading failed [{$this->host}:{$this->port}](1)"); 205 | } elseif ($recv === '') { 206 | throw new NetworkSocketException("Reading failed [{$this->host}:{$this->port}](2)"); 207 | } else { 208 | $buffer .= $recv; 209 | $remainingLen -= strlen($recv); 210 | } 211 | } elseif ($readable === 0) { 212 | throw new NetworkTimeoutException("Reading timeout [{$this->host}:{$this->port}]"); 213 | } else { 214 | throw new NetworkSocketException("Reading failed [{$this->host}:{$this->port}](3)"); 215 | } 216 | } 217 | 218 | return $buffer; 219 | } 220 | 221 | /** 222 | * @return bool 223 | */ 224 | public function close() 225 | { 226 | $closed = fclose($this->socket); 227 | $this->socket = null; 228 | return $closed; 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/Contract/Message.php: -------------------------------------------------------------------------------- 1 | '0;31m', // red 35 | LogLevel::ALERT => '0;31m', // red 36 | LogLevel::CRITICAL => '0;31m', // red 37 | LogLevel::ERROR => '0;31m', // red 38 | LogLevel::WARNING => '1;33m', // yellow 39 | LogLevel::NOTICE => '0;35m', // purple 40 | LogLevel::INFO => '0;36m', // cyan 41 | LogLevel::DEBUG => '0;32m', // green 42 | ]; 43 | 44 | /** 45 | * @var string 46 | */ 47 | private $colorCtxKey = '0;37m'; // light gray 48 | 49 | /** 50 | * @var string 51 | */ 52 | private $colorMsg = '1;37m'; // white 53 | 54 | /** 55 | * @var string 56 | */ 57 | private $colorNO = "\033[0m"; 58 | 59 | /** 60 | * @var string 61 | */ 62 | private $colorBGN = "\033["; 63 | 64 | /** 65 | * @var array 66 | */ 67 | private $allows = []; 68 | 69 | /** 70 | * EchoLogger constructor. 71 | * @param string $minimalLevel 72 | */ 73 | public function __construct($minimalLevel = LogLevel::NOTICE) 74 | { 75 | $this->allows = array_slice($this->levels, 0, array_search($minimalLevel, $this->levels, true) + 1); 76 | } 77 | 78 | /** 79 | * @param mixed $level 80 | * @param string $message 81 | * @param array $context 82 | */ 83 | public function log($level, $message, array $context = array()) 84 | { 85 | if (in_array($level, $this->allows)) { 86 | printf( 87 | '[%s]%s[%s] : %s ~ %s %s', 88 | $this->printableLevel($level), 89 | " ", 90 | date('Y-m-d H:i:s'), 91 | $this->printableMessage($message), 92 | $this->printableContext($context), 93 | "\n" 94 | ); 95 | } 96 | } 97 | 98 | /** 99 | * @param $level 100 | * @return string 101 | */ 102 | private function printableLevel($level) 103 | { 104 | return $this->colorBGN . $this->colors[$level] . strtoupper($level) . $this->colorNO; 105 | } 106 | 107 | /** 108 | * @param $message 109 | * @return string 110 | */ 111 | private function printableMessage($message) 112 | { 113 | return $this->colorBGN . $this->colorMsg . $message . $this->colorNO; 114 | } 115 | 116 | /** 117 | * @param $context 118 | * @return string 119 | */ 120 | private function printableContext($context) 121 | { 122 | $print = '['; 123 | 124 | array_walk($context, function ($item, $key) use (&$print) { 125 | $ctx = $this->colorBGN . $this->colorCtxKey . $key . $this->colorNO . '='; 126 | if (is_array($item)) { 127 | $ctx .= json_encode($item, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); 128 | } else { 129 | $ctx .= $item; 130 | } 131 | $print .= $ctx . ','; 132 | }); 133 | 134 | return $print . ']'; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/Logger/Logger.php: -------------------------------------------------------------------------------- 1 | nullLogger = new NullLogger; 44 | } 45 | 46 | /** 47 | * @param mixed $level 48 | * @param string $message 49 | * @param array $context 50 | */ 51 | public function log($level, $message, array $context = array()) 52 | { 53 | if (SDK::$presentLogger) { 54 | SDK::$presentLogger->log($level, $message, $context); 55 | } else { 56 | $this->nullLogger->log($level, $message, $context); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Message/Bag.php: -------------------------------------------------------------------------------- 1 | append(new Message($item)); 27 | } 28 | return $bag; 29 | } 30 | 31 | /** 32 | * @param $msg 33 | */ 34 | public function append($msg) 35 | { 36 | $this->messages[] = $msg; 37 | } 38 | 39 | /** 40 | * @return array 41 | */ 42 | public function export() 43 | { 44 | $bag = []; 45 | foreach ($this->messages as $msg) { 46 | $bag[] = $msg->data(); 47 | } 48 | return $bag; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Message/Message.php: -------------------------------------------------------------------------------- 1 | id = $id; 62 | $this->payload = $payload; 63 | $this->attempts = $attempts; 64 | $this->timestamp = $timestamp; 65 | $this->data = $id ? json_decode($payload, true) : json_encode($payload); 66 | $this->nsqd = $nsqd; 67 | } 68 | 69 | /** 70 | * @return string 71 | */ 72 | public function id() 73 | { 74 | return $this->id; 75 | } 76 | 77 | /** 78 | * @return string 79 | */ 80 | public function payload() 81 | { 82 | return $this->payload; 83 | } 84 | 85 | /** 86 | * @return mixed 87 | */ 88 | public function data() 89 | { 90 | return $this->data; 91 | } 92 | 93 | /** 94 | * @return int 95 | */ 96 | public function attempts() 97 | { 98 | return $this->attempts; 99 | } 100 | 101 | /** 102 | * @return int 103 | */ 104 | public function timestamp() 105 | { 106 | return $this->timestamp; 107 | } 108 | 109 | /** 110 | * just done 111 | */ 112 | public function done() 113 | { 114 | $this->nsqd->finish($this->id); 115 | } 116 | 117 | /** 118 | * just retry 119 | */ 120 | public function retry() 121 | { 122 | $this->delay(0); 123 | } 124 | 125 | /** 126 | * just delay 127 | * @param $seconds 128 | */ 129 | public function delay($seconds) 130 | { 131 | $this->nsqd->requeue($this->id, $seconds * 1000); 132 | } 133 | 134 | /** 135 | * @param $seconds 136 | * @return null|int|static 137 | */ 138 | public function deferred($seconds = null) 139 | { 140 | if (is_null($seconds)) { 141 | return $this->deferred; 142 | } else { 143 | $this->deferred = $seconds * 1000; 144 | return $this; 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/Protocol/Binary.php: -------------------------------------------------------------------------------- 1 | read(2)); 24 | if (is_array($unpack)) { 25 | list(, $res) = $unpack; 26 | return $res; 27 | } else { 28 | return null; 29 | } 30 | } 31 | 32 | /** 33 | * Read and unpack integer (4 bytes) from buffer 34 | * @param Stream $buffer 35 | * @return int 36 | */ 37 | public static function readInt(Stream $buffer) 38 | { 39 | $unpack = @unpack('N', $buffer->read(4)); 40 | if (is_array($unpack)) { 41 | list(, $res) = $unpack; 42 | if (PHP_INT_SIZE !== 4) { 43 | $res = sprintf('%u', $res); 44 | } 45 | return (int)$res; 46 | } else { 47 | return null; 48 | } 49 | } 50 | 51 | /** 52 | * Read and unpack long (8 bytes) from buffer 53 | * @param Stream $buffer 54 | * @return string 55 | */ 56 | public static function readLong(Stream $buffer) 57 | { 58 | $hi = @unpack('N', $buffer->read(4)); 59 | $lo = @unpack('N', $buffer->read(4)); 60 | if (is_array($hi) && is_array($lo)) { 61 | $hi = sprintf('%u', $hi[1]); 62 | $lo = sprintf('%u', $lo[1]); 63 | return bcadd(bcmul($hi, '4294967296'), $lo); 64 | } else { 65 | return null; 66 | } 67 | } 68 | 69 | /** 70 | * Read and unpack string 71 | * @param Stream $buffer 72 | * @param int $size 73 | * @return string 74 | */ 75 | public static function readString(Stream $buffer, $size) 76 | { 77 | if (!SDK::$enabledStringPack) { 78 | return $buffer->read($size); 79 | } 80 | 81 | $temp = @unpack('c'.$size.'chars', $buffer->read($size)); 82 | if (is_array($temp)) { 83 | $out = ''; 84 | foreach ($temp as $v) { 85 | if ($v > 0) { 86 | $out .= chr($v); 87 | } 88 | } 89 | return $out; 90 | } else { 91 | return null; 92 | } 93 | } 94 | 95 | /** 96 | * Pack string 97 | * @param $data 98 | * @return string 99 | */ 100 | public static function packString($data) 101 | { 102 | if (!SDK::$enabledStringPack) { 103 | return $data; 104 | } 105 | 106 | $out = ''; 107 | $len = strlen($data); 108 | for ($i = 0; $i < $len; $i++) { 109 | $out .= pack('c', ord(substr($data, $i, 1))); 110 | } 111 | return $out; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Protocol/Command.php: -------------------------------------------------------------------------------- 1 | (string)$client_id, 39 | 'hostname' => (string)$hostname, 40 | 'user_agent' => (string)$user_agent 41 | ]); 42 | $size = pack('N', strlen($data)); 43 | return $cmd . $size . $data; 44 | } 45 | 46 | /** 47 | * Subscribe [SUB] 48 | * @param string $topic 49 | * @param string $channel 50 | * @return string 51 | */ 52 | public static function subscribe($topic, $channel) 53 | { 54 | return self::command('SUB', $topic, $channel); 55 | } 56 | 57 | /** 58 | * Publish [PUB] 59 | * @param string $topic 60 | * @param string $message 61 | * @param int $deferred 62 | * @return string 63 | */ 64 | public static function message($topic, $message, $deferred = null) 65 | { 66 | $cmd = is_null($deferred) 67 | ? self::command('PUB', $topic) 68 | : self::command('DPUB', $topic, $deferred); 69 | $data = Binary::packString($message); 70 | $size = pack('N', strlen($data)); 71 | return $cmd . $size . $data; 72 | } 73 | 74 | /** 75 | * Publish -multi [MPUB] 76 | * @param $topic 77 | * @param $messages 78 | * @return string 79 | */ 80 | public static function messages($topic, $messages) 81 | { 82 | $cmd = self::command('MPUB', $topic); 83 | $msgNum = pack('N', count($messages)); 84 | $buffer = ''; 85 | foreach ($messages as $message) { 86 | $data = Binary::packString($message); 87 | $size = pack('N', strlen($data)); 88 | $buffer .= $size . $data; 89 | } 90 | $bodySize = pack('N', strlen($msgNum . $buffer)); 91 | return $cmd . $bodySize . $msgNum . $buffer; 92 | } 93 | 94 | /** 95 | * Ready [RDY] 96 | * @param integer $count 97 | * @return string 98 | */ 99 | public static function ready($count) 100 | { 101 | return self::command('RDY', $count); 102 | } 103 | 104 | /** 105 | * Finish [FIN] 106 | * @param string $id 107 | * @return string 108 | */ 109 | public static function finish($id) 110 | { 111 | return self::command('FIN', $id); 112 | } 113 | 114 | /** 115 | * Requeue [REQ] 116 | * @param string $id 117 | * @param integer $millisecond 118 | * @return string 119 | */ 120 | public static function requeue($id, $millisecond) 121 | { 122 | return self::command('REQ', $id, $millisecond); 123 | } 124 | 125 | /** 126 | * No-op [NOP] 127 | * @return string 128 | */ 129 | public static function nop() 130 | { 131 | return self::command('NOP'); 132 | } 133 | 134 | /** 135 | * Cleanly close [CLS] 136 | * @return string 137 | */ 138 | public static function close() 139 | { 140 | return self::command('CLS'); 141 | } 142 | 143 | /** 144 | * Gen command 145 | * @return string 146 | */ 147 | private static function command() 148 | { 149 | $args = func_get_args(); 150 | $cmd = array_shift($args); 151 | return sprintf('%s %s%s', $cmd, implode(' ', $args), "\n"); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/Protocol/CommandHTTP.php: -------------------------------------------------------------------------------- 1 | $frameType, 'size' => $size]; 50 | 51 | // switch 52 | switch ($frameType) { 53 | case self::FRAME_TYPE_RESPONSE: 54 | $frame['response'] = Binary::readString($buffer, $size - 4); 55 | break; 56 | case self::FRAME_TYPE_ERROR: 57 | $frame['error'] = Binary::readString($buffer, $size - 4); 58 | break; 59 | case self::FRAME_TYPE_MESSAGE: 60 | $frame['timestamp'] = Binary::readLong($buffer); 61 | $frame['attempts'] = Binary::readShort($buffer); 62 | $frame['id'] = Binary::readString($buffer, 16); 63 | $frame['payload'] = Binary::readString($buffer, $size - 30); 64 | break; 65 | default: 66 | throw new UnknownFrameException(Binary::readString($buffer, $size - 4)); 67 | break; 68 | } 69 | 70 | // check frame data 71 | foreach ($frame as $k => $val) { 72 | if (is_null($val)) { 73 | $frame['type'] = self::FRAME_TYPE_BROKEN; 74 | $frame['error'] = 'broken frame (maybe network error)'; 75 | break; 76 | } 77 | } 78 | 79 | return $frame; 80 | } 81 | 82 | /** 83 | * Test if frame is a message 84 | * @param array $frame 85 | * @return bool 86 | */ 87 | public static function frameIsMessage(array $frame) 88 | { 89 | return isset($frame['type'], $frame['payload']) && $frame['type'] === self::FRAME_TYPE_MESSAGE; 90 | } 91 | 92 | /** 93 | * Test if frame is HEARTBEAT 94 | * @param array $frame 95 | * @return bool 96 | */ 97 | public static function frameIsHeartbeat(array $frame) 98 | { 99 | return self::frameIsResponse($frame, self::HEARTBEAT); 100 | } 101 | 102 | /** 103 | * Test if frame is OK 104 | * @param array $frame 105 | * @return bool 106 | */ 107 | public static function frameIsOK(array $frame) 108 | { 109 | return self::frameIsResponse($frame, self::OK); 110 | } 111 | 112 | /** 113 | * Test if frame is CLOSE_WAIT 114 | * @param array $frame 115 | * @return bool 116 | */ 117 | public static function frameIsCloseWait(array $frame) 118 | { 119 | return self::frameIsResponse($frame, self::CLOSE_WAIT); 120 | } 121 | 122 | /** 123 | * Test if frame is ERROR 124 | * @param array $frame 125 | * @return bool 126 | */ 127 | public static function frameIsError(array $frame) 128 | { 129 | return isset($frame['type']) && $frame['type'] === self::FRAME_TYPE_ERROR && isset($frame['error']); 130 | } 131 | 132 | /** 133 | * Test if frame is BROKEN 134 | * @param array $frame 135 | * @return bool 136 | */ 137 | public static function frameIsBroken(array $frame) 138 | { 139 | return isset($frame['type']) && $frame['type'] === self::FRAME_TYPE_BROKEN; 140 | } 141 | 142 | /** 143 | * Test if frame is a response frame (optionally with content $response) 144 | * @param array $frame 145 | * @param string 146 | * @return bool 147 | */ 148 | private static function frameIsResponse(array $frame, $response = null) 149 | { 150 | return 151 | isset($frame['type'], $frame['response']) 152 | && 153 | $frame['type'] === self::FRAME_TYPE_RESPONSE 154 | && 155 | ($response === null || $frame['response'] === $response); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/Queue.php: -------------------------------------------------------------------------------- 1 | info('Creating new nsqd for producer', [ 36 | 'lookupd' => $endpoint->getLookupd(), 37 | 'route' => $route 38 | ]); 39 | return (new Nsqd($endpoint)) 40 | ->setRoute($route) 41 | ->setLifecycle(SDK::$pubRecyclingSec) 42 | ->setProducer(); 43 | }) 44 | ->setTopic($topic) 45 | ->publish($message) 46 | ; 47 | } 48 | 49 | /** 50 | * @param Endpoint $endpoint 51 | * @param string $topic 52 | * @param string $channel 53 | * @param callable $processor 54 | * @param int $lifecycle 55 | */ 56 | public static function subscribe(Endpoint $endpoint, $topic, $channel, callable $processor, $lifecycle = 0) 57 | { 58 | $routes = Lookupd::getNodes($endpoint, $topic); 59 | 60 | foreach ($routes as $route) { 61 | $keys = [$route['topic'], $route['host'], $route['ports']['tcp']]; 62 | 63 | Pool::register($keys, function () use ($endpoint, $route, $topic, $processor, $lifecycle) { 64 | 65 | Logger::ins()->info('Creating new nsqd for consumer', [ 66 | 'lookupd' => $endpoint->getLookupd(), 67 | 'route' => $route, 68 | 'topic' => $topic, 69 | 'lifecycle' => $lifecycle 70 | ]); 71 | return (new Nsqd($endpoint)) 72 | ->setRoute($route) 73 | ->setTopic($topic) 74 | ->setLifecycle($lifecycle) 75 | ->setConsumer($processor); 76 | }) 77 | ->subscribe($channel) 78 | ; 79 | } 80 | 81 | Pool::getEvLoop()->run(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/SDK.php: -------------------------------------------------------------------------------- 1 | 'SIGHUP', 28 | SIGINT => 'SIGINT', 29 | SIGTERM => 'SIGTERM' 30 | ]; 31 | 32 | /** 33 | * @param LoopInterface $evLoop 34 | */ 35 | public static function init(LoopInterface $evLoop) 36 | { 37 | if (extension_loaded('pcntl')) { 38 | foreach (self::$acceptSignals as $signal => $name) { 39 | pcntl_signal($signal, [__CLASS__, 'signalHandler']); 40 | } 41 | 42 | $evLoop->addPeriodicTimer(self::$signalDispatchInv, function () { 43 | pcntl_signal_dispatch(); 44 | }); 45 | } 46 | } 47 | 48 | /** 49 | * @param $signal 50 | */ 51 | public static function signalHandler($signal) 52 | { 53 | Logger::ins()->info('Signal ['.self::$acceptSignals[$signal].'] received .. prepare shutdown'); 54 | 55 | $instances = Pool::instances(); 56 | foreach ($instances as $nsqdIns) { 57 | $nsqdIns->isConsumer() && $nsqdIns->closing(); 58 | } 59 | } 60 | } 61 | --------------------------------------------------------------------------------