Initial commit
This commit is contained in:
122
.github/workflows/ci.yaml
vendored
Normal file
122
.github/workflows/ci.yaml
vendored
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
- pull_request
|
||||||
|
- push
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
tests:
|
||||||
|
name: Tests
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os:
|
||||||
|
- ubuntu-latest
|
||||||
|
php:
|
||||||
|
- '7.4'
|
||||||
|
- '8.0'
|
||||||
|
dependencies:
|
||||||
|
- lowest
|
||||||
|
- highest
|
||||||
|
|
||||||
|
services:
|
||||||
|
nsqd:
|
||||||
|
image: nsqio/nsq:v1.2.0
|
||||||
|
options: --entrypoint /nsqd
|
||||||
|
ports:
|
||||||
|
- 4150:4150
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: ${{ matrix.php }}
|
||||||
|
coverage: pcov
|
||||||
|
|
||||||
|
- name: Setup Problem Matchers for PHPUnit
|
||||||
|
run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
|
||||||
|
|
||||||
|
- name: Determine Composer cache directory
|
||||||
|
id: composer-cache
|
||||||
|
run: echo "::set-output name=directory::$(composer config cache-dir)"
|
||||||
|
|
||||||
|
- name: Cache Composer dependencies
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ${{ steps.composer-cache.outputs.directory }}
|
||||||
|
key: ${{ runner.os }}-${{ matrix.php }}-composer-${{ matrix.dependencies }}-${{ hashFiles('**/composer.lock') }}
|
||||||
|
restore-keys: ${{ runner.os }}-${{ matrix.php }}-${{ matrix.dependencies }}-composer-
|
||||||
|
|
||||||
|
- name: Install highest dependencies
|
||||||
|
run: composer update --no-progress --no-interaction --prefer-dist
|
||||||
|
if: ${{ matrix.dependencies == 'highest' }}
|
||||||
|
|
||||||
|
- name: Install lowest dependencies
|
||||||
|
run: composer update --no-progress --no-interaction --prefer-dist --prefer-lowest
|
||||||
|
if: ${{ matrix.dependencies == 'lowest' }}
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: vendor/bin/phpunit --coverage-clover=build/coverage-report.xml
|
||||||
|
|
||||||
|
- name: Upload code coverage
|
||||||
|
uses: codecov/codecov-action@v1
|
||||||
|
with:
|
||||||
|
file: build/coverage-report.xml
|
||||||
|
|
||||||
|
php-cs-fixer:
|
||||||
|
name: PHP-CS-Fixer
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '7.4'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: composer update --no-progress --no-interaction --prefer-dist
|
||||||
|
|
||||||
|
- name: Run script
|
||||||
|
run: composer phpcs
|
||||||
|
|
||||||
|
phpstan:
|
||||||
|
name: PHPStan
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '7.4'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: composer update --no-progress --no-interaction --prefer-dist
|
||||||
|
|
||||||
|
- name: Run script
|
||||||
|
run: composer phpstan
|
||||||
|
|
||||||
|
psalm:
|
||||||
|
name: Psalm
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '7.4'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: composer update --no-progress --no-interaction --prefer-dist
|
||||||
|
|
||||||
|
- name: Run script
|
||||||
|
run: composer psalm
|
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/vendor/
|
||||||
|
/composer.lock
|
19
.php_cs.dist
Normal file
19
.php_cs.dist
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return (new PhpCsFixer\Config())
|
||||||
|
->setRiskyAllowed(true)
|
||||||
|
->setRules([
|
||||||
|
'@PhpCsFixer' => true,
|
||||||
|
'@PhpCsFixer:risky' => true,
|
||||||
|
'@PSR12' => true,
|
||||||
|
'@PSR12:risky' => true,
|
||||||
|
'declare_strict_types' => true,
|
||||||
|
'php_unit_internal_class' => false,
|
||||||
|
'php_unit_test_class_requires_covers' => false,
|
||||||
|
'yoda_style' => true,
|
||||||
|
])
|
||||||
|
->setFinder(
|
||||||
|
PhpCsFixer\Finder::create()
|
||||||
|
->in(__DIR__)
|
||||||
|
);
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2021 nsqphp
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
42
README.md
Normal file
42
README.md
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# Nsq PHP
|
||||||
|
|
||||||
|
<img src="https://github.com/nsqphp/nsqphp/raw/main/logo.png" alt="" align="left" width="150">
|
||||||
|
|
||||||
|
A NSQ Client library for PHP.
|
||||||
|
|
||||||
|
[](//packagist.org/packages/nsq/nsq) [](//packagist.org/packages/nsq/nsq) [](//packagist.org/packages/nsq/nsq) [](//packagist.org/packages/nsq/nsq)
|
||||||
|
[](https://codecov.io/gh/nsqphp/nsqphp)
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
This library is installable via [Composer](https://getcomposer.org/):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer require nsq/nsq
|
||||||
|
```
|
||||||
|
|
||||||
|
Requirements
|
||||||
|
------------
|
||||||
|
|
||||||
|
This library requires PHP 7.4 or later.
|
||||||
|
|
||||||
|
Although not required, it is recommended that you install the [phpinnacle/ext-buffer](https://github.com/phpinnacle/ext-buffer) to speed up [phpinnacle/buffer](https://github.com/phpinnacle/buffer) .
|
||||||
|
|
||||||
|
Features
|
||||||
|
--------
|
||||||
|
|
||||||
|
- [x] SUB
|
||||||
|
- [x] PUB
|
||||||
|
- [ ] Feature Negotiation
|
||||||
|
- [ ] Discovery
|
||||||
|
- [ ] Backoff
|
||||||
|
- [ ] TLS
|
||||||
|
- [ ] Snappy
|
||||||
|
- [ ] Sampling
|
||||||
|
- [ ] AUTH
|
||||||
|
|
||||||
|
License:
|
||||||
|
--------
|
||||||
|
|
||||||
|
The MIT License (MIT). Please see [`LICENSE`](./LICENSE) for more information. Maintained by [Spiral Scout](https://spiralscout.com).
|
52
composer.json
Normal file
52
composer.json
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"name": "nsq/nsq",
|
||||||
|
"type": "library",
|
||||||
|
"description": "NSQ Client for PHP",
|
||||||
|
"homepage": "https://github.com/nsqphp/nsqphp",
|
||||||
|
"license": "MIT",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Konstantin Grachev",
|
||||||
|
"email": "me@grachevko.ru"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"php": ">=7.4",
|
||||||
|
"ext-json": "*",
|
||||||
|
"clue/socket-raw": "^1.5",
|
||||||
|
"phpinnacle/buffer": "^1.2"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"ergebnis/composer-normalize": "9999999-dev",
|
||||||
|
"friendsofphp/php-cs-fixer": "^2.18",
|
||||||
|
"phpstan/phpstan": "^0.12.68",
|
||||||
|
"phpstan/phpstan-phpunit": "^0.12.17",
|
||||||
|
"phpstan/phpstan-strict-rules": "^0.12.9",
|
||||||
|
"phpunit/phpunit": "^9.5",
|
||||||
|
"vimeo/psalm": "^4.4"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"sort-packages": true
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Nsq\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minimum-stability": "dev",
|
||||||
|
"prefer-stable": true,
|
||||||
|
"scripts": {
|
||||||
|
"phpcs": [
|
||||||
|
"vendor/bin/php-cs-fixer fix --verbose --diff --dry-run"
|
||||||
|
],
|
||||||
|
"phpstan": [
|
||||||
|
"vendor/bin/phpstan analyse"
|
||||||
|
],
|
||||||
|
"psalm": [
|
||||||
|
"vendor/bin/psalm"
|
||||||
|
],
|
||||||
|
"tests": [
|
||||||
|
"vendor/bin/phpunit --verbose"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
14
docker-compose.yml
Normal file
14
docker-compose.yml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
version: '3.7'
|
||||||
|
|
||||||
|
services:
|
||||||
|
nsqd:
|
||||||
|
image: nsqio/nsq:v1.2.0
|
||||||
|
command: /nsqd
|
||||||
|
ports:
|
||||||
|
- 4150:4150
|
||||||
|
|
||||||
|
nsqadmin:
|
||||||
|
image: nsqio/nsq:v1.2.0
|
||||||
|
command: /nsqadmin --nsqd-http-address=nsqd:4151 --http-address=0.0.0.0:4171
|
||||||
|
ports:
|
||||||
|
- 4171:4171
|
10
phpstan.neon
Normal file
10
phpstan.neon
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
includes:
|
||||||
|
- vendor/phpstan/phpstan-phpunit/extension.neon
|
||||||
|
- vendor/phpstan/phpstan-strict-rules/rules.neon
|
||||||
|
- vendor/phpstan/phpstan/conf/bleedingEdge.neon
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
level: 7
|
||||||
|
paths:
|
||||||
|
- src
|
||||||
|
- tests
|
20
phpunit.xml.dist
Normal file
20
phpunit.xml.dist
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phpunit
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.5/phpunit.xsd"
|
||||||
|
colors="true"
|
||||||
|
>
|
||||||
|
<coverage processUncoveredFiles="true">
|
||||||
|
<include>
|
||||||
|
<directory suffix=".php">./src/</directory>
|
||||||
|
</include>
|
||||||
|
</coverage>
|
||||||
|
<php>
|
||||||
|
<ini name="error_reporting" value="-1"/>
|
||||||
|
</php>
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Nsq Test Suite">
|
||||||
|
<directory>tests</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
</phpunit>
|
16
psalm.xml
Normal file
16
psalm.xml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<psalm
|
||||||
|
allowPhpStormGenerics="true"
|
||||||
|
ignoreInternalFunctionFalseReturn="false"
|
||||||
|
ignoreInternalFunctionNullReturn="false"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns="https://getpsalm.org/schema/config"
|
||||||
|
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
|
||||||
|
>
|
||||||
|
<projectFiles>
|
||||||
|
<directory name="src"/>
|
||||||
|
<ignoreFiles>
|
||||||
|
<directory name="vendor"/>
|
||||||
|
</ignoreFiles>
|
||||||
|
</projectFiles>
|
||||||
|
</psalm>
|
19
src/Config.php
Normal file
19
src/Config.php
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Nsq;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @psalm-immutable
|
||||||
|
*/
|
||||||
|
final class Config
|
||||||
|
{
|
||||||
|
public string $address;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
string $address
|
||||||
|
) {
|
||||||
|
$this->address = $address;
|
||||||
|
}
|
||||||
|
}
|
136
src/Connection.php
Normal file
136
src/Connection.php
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Nsq;
|
||||||
|
|
||||||
|
use LogicException;
|
||||||
|
use PHPinnacle\Buffer\ByteBuffer;
|
||||||
|
use Socket\Raw\Factory;
|
||||||
|
use Socket\Raw\Socket;
|
||||||
|
use Throwable;
|
||||||
|
use function json_encode;
|
||||||
|
use function pack;
|
||||||
|
use function sprintf;
|
||||||
|
use const JSON_FORCE_OBJECT;
|
||||||
|
use const JSON_THROW_ON_ERROR;
|
||||||
|
use const PHP_EOL;
|
||||||
|
|
||||||
|
final class Connection
|
||||||
|
{
|
||||||
|
private const OK = 'OK';
|
||||||
|
private const HEARTBEAT = '_heartbeat_';
|
||||||
|
private const CLOSE_WAIT = 'CLOSE_WAIT';
|
||||||
|
private const TYPE_RESPONSE = 0;
|
||||||
|
private const TYPE_ERROR = 1;
|
||||||
|
private const TYPE_MESSAGE = 2;
|
||||||
|
private const BYTES_SIZE = 4;
|
||||||
|
private const BYTES_TYPE = 4;
|
||||||
|
private const BYTES_ATTEMPTS = 2;
|
||||||
|
private const BYTES_TIMESTAMP = 8;
|
||||||
|
private const BYTES_ID = 16;
|
||||||
|
private const MAGIC_V2 = ' V2';
|
||||||
|
|
||||||
|
public Socket $socket;
|
||||||
|
|
||||||
|
public bool $closed = false;
|
||||||
|
|
||||||
|
private function __construct(Socket $socket)
|
||||||
|
{
|
||||||
|
$this->socket = $socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @psalm-suppress UnsafeInstantiation
|
||||||
|
*
|
||||||
|
* @return static
|
||||||
|
*/
|
||||||
|
public static function connect(Config $config): self
|
||||||
|
{
|
||||||
|
$socket = (new Factory())->createClient($config->address);
|
||||||
|
$socket->write(self::MAGIC_V2);
|
||||||
|
|
||||||
|
// @phpstan-ignore-next-line
|
||||||
|
return new self($socket);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @psalm-param array<string, string|numeric> $arr
|
||||||
|
*
|
||||||
|
* @psalm-suppress PossiblyFalseOperand
|
||||||
|
*/
|
||||||
|
public function identify(array $arr): string
|
||||||
|
{
|
||||||
|
$body = json_encode($arr, JSON_THROW_ON_ERROR | JSON_FORCE_OBJECT);
|
||||||
|
$size = pack('N', \strlen($body));
|
||||||
|
|
||||||
|
return 'IDENTIFY '.PHP_EOL.$size.$body;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @psalm-suppress PossiblyFalseOperand
|
||||||
|
*/
|
||||||
|
public function auth(string $secret): string
|
||||||
|
{
|
||||||
|
$size = pack('N', \strlen($secret));
|
||||||
|
|
||||||
|
return 'AUTH'.PHP_EOL.$size.$secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function write(string $buffer): void
|
||||||
|
{
|
||||||
|
if ($this->closed) {
|
||||||
|
throw new LogicException('This connection is closed, create new one.');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->socket->write($buffer);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$this->closed = true;
|
||||||
|
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function read(): ?Message
|
||||||
|
{
|
||||||
|
$socket = $this->socket;
|
||||||
|
|
||||||
|
$buffer = new ByteBuffer($socket->read(self::BYTES_SIZE + self::BYTES_TYPE));
|
||||||
|
$size = $buffer->consumeUint32();
|
||||||
|
$type = $buffer->consumeUint32();
|
||||||
|
|
||||||
|
$buffer->append($socket->read($size - self::BYTES_TYPE));
|
||||||
|
|
||||||
|
if (self::TYPE_RESPONSE === $type) {
|
||||||
|
$response = $buffer->consume($size - self::BYTES_TYPE);
|
||||||
|
|
||||||
|
if (self::OK === $response || self::CLOSE_WAIT === $response) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self::HEARTBEAT === $response) {
|
||||||
|
$socket->write('NOP'.PHP_EOL);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new LogicException(sprintf('Unexpected response from nsq: "%s"', $response));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self::TYPE_ERROR === $type) {
|
||||||
|
throw new LogicException(sprintf('NSQ return error: "%s"', $socket->read($size)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self::TYPE_MESSAGE !== $type) {
|
||||||
|
throw new LogicException(sprintf('Expecting "%s" type, but NSQ return: "%s"', self::TYPE_MESSAGE, $type));
|
||||||
|
}
|
||||||
|
|
||||||
|
$timestamp = $buffer->consumeInt64();
|
||||||
|
$attempts = $buffer->consumeUint16();
|
||||||
|
$id = $buffer->consume(self::BYTES_ID);
|
||||||
|
$body = $buffer->consume($size - self::BYTES_TYPE - self::BYTES_TIMESTAMP - self::BYTES_ATTEMPTS - self::BYTES_ID);
|
||||||
|
|
||||||
|
return new Message($timestamp, $attempts, $id, $body);
|
||||||
|
}
|
||||||
|
}
|
51
src/Envelope.php
Normal file
51
src/Envelope.php
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Nsq;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @psalm-immutable
|
||||||
|
*/
|
||||||
|
final class Envelope
|
||||||
|
{
|
||||||
|
public Message $message;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var callable
|
||||||
|
*/
|
||||||
|
private $acknowledge;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var callable
|
||||||
|
*/
|
||||||
|
private $requeue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var callable
|
||||||
|
*/
|
||||||
|
private $touching;
|
||||||
|
|
||||||
|
public function __construct(Message $message, callable $ack, callable $req, callable $touch)
|
||||||
|
{
|
||||||
|
$this->message = $message;
|
||||||
|
$this->acknowledge = $ack;
|
||||||
|
$this->requeue = $req;
|
||||||
|
$this->touching = $touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ack(): void
|
||||||
|
{
|
||||||
|
\call_user_func($this->acknowledge);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function retry(int $timeout): void
|
||||||
|
{
|
||||||
|
\call_user_func($this->requeue, $timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function touch(): void
|
||||||
|
{
|
||||||
|
\call_user_func($this->touching);
|
||||||
|
}
|
||||||
|
}
|
27
src/Message.php
Normal file
27
src/Message.php
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Nsq;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @psalm-immutable
|
||||||
|
*/
|
||||||
|
final class Message
|
||||||
|
{
|
||||||
|
public int $timestamp;
|
||||||
|
|
||||||
|
public int $attempts;
|
||||||
|
|
||||||
|
public string $id;
|
||||||
|
|
||||||
|
public string $body;
|
||||||
|
|
||||||
|
public function __construct(int $timestamp, int $attempts, string $id, string $body)
|
||||||
|
{
|
||||||
|
$this->timestamp = $timestamp;
|
||||||
|
$this->attempts = $attempts;
|
||||||
|
$this->id = $id;
|
||||||
|
$this->body = $body;
|
||||||
|
}
|
||||||
|
}
|
99
src/Reader.php
Normal file
99
src/Reader.php
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Nsq;
|
||||||
|
|
||||||
|
use Throwable;
|
||||||
|
use function sprintf;
|
||||||
|
use const PHP_EOL;
|
||||||
|
|
||||||
|
class Reader
|
||||||
|
{
|
||||||
|
private Connection $connection;
|
||||||
|
|
||||||
|
public function __construct(Connection $connection)
|
||||||
|
{
|
||||||
|
$this->connection = $connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __destruct()
|
||||||
|
{
|
||||||
|
$this->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to a topic/channel.
|
||||||
|
*/
|
||||||
|
public function sub(string $topic, string $channel): void
|
||||||
|
{
|
||||||
|
$buffer = sprintf('SUB %s %s', $topic, $channel).PHP_EOL;
|
||||||
|
|
||||||
|
$this->connection->write($buffer);
|
||||||
|
$this->connection->read();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update RDY state (indicate you are ready to receive N messages).
|
||||||
|
*/
|
||||||
|
public function rdy(int $count): void
|
||||||
|
{
|
||||||
|
$this->connection->write('RDY '.$count.PHP_EOL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finish a message (indicate successful processing).
|
||||||
|
*/
|
||||||
|
public function fin(string $id): void
|
||||||
|
{
|
||||||
|
$this->connection->write('FIN '.$id.PHP_EOL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-queue a message (indicate failure to process)
|
||||||
|
* The re-queued message is placed at the tail of the queue, equivalent to having just published it,
|
||||||
|
* but for various implementation specific reasons that behavior should not be explicitly relied upon and may change in the future.
|
||||||
|
* Similarly, a message that is in-flight and times out behaves identically to an explicit REQ.
|
||||||
|
*/
|
||||||
|
public function req(string $id, int $timeout): void
|
||||||
|
{
|
||||||
|
$this->connection->write(sprintf('REQ %s %s', $id, $timeout).PHP_EOL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the timeout for an in-flight message.
|
||||||
|
*/
|
||||||
|
public function touch(string $id): void
|
||||||
|
{
|
||||||
|
$this->connection->write('TOUCH '.$id.PHP_EOL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function consume(?float $timeout = null): ?Message
|
||||||
|
{
|
||||||
|
if (false === $this->connection->socket->selectRead($timeout)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->connection->read() ?? $this->consume(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanly close your connection (no more messages are sent).
|
||||||
|
*/
|
||||||
|
public function close(): void
|
||||||
|
{
|
||||||
|
if ($this->connection->closed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->connection->closed = true;
|
||||||
|
|
||||||
|
$this->connection->socket->write('CLS'.PHP_EOL);
|
||||||
|
$this->connection->read();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->connection->socket->close();
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
74
src/Subscriber.php
Normal file
74
src/Subscriber.php
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Nsq;
|
||||||
|
|
||||||
|
use Generator;
|
||||||
|
use LogicException;
|
||||||
|
|
||||||
|
final class Subscriber
|
||||||
|
{
|
||||||
|
private Reader $reader;
|
||||||
|
|
||||||
|
public function __construct(Reader $reader)
|
||||||
|
{
|
||||||
|
$this->reader = $reader;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @psalm-return Generator<int, Envelope|null, true|null, void>
|
||||||
|
*/
|
||||||
|
public function subscribe(string $topic, string $channel, ?float $timeout = 0): Generator
|
||||||
|
{
|
||||||
|
$reader = $this->reader;
|
||||||
|
$reader->sub($topic, $channel);
|
||||||
|
$reader->rdy(1);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
$message = $reader->consume($timeout);
|
||||||
|
|
||||||
|
if (null === $message) {
|
||||||
|
if (true === yield null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$finished = false;
|
||||||
|
$envelop = new Envelope(
|
||||||
|
$message,
|
||||||
|
static function () use ($reader, $message, &$finished): void {
|
||||||
|
if ($finished) {
|
||||||
|
throw new LogicException('Can\'t ack, message already finished.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$finished = true;
|
||||||
|
|
||||||
|
$reader->fin($message->id);
|
||||||
|
},
|
||||||
|
static function (int $timeout) use ($reader, $message, &$finished): void {
|
||||||
|
if ($finished) {
|
||||||
|
throw new LogicException('Can\'t retry, message already finished.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$finished = true;
|
||||||
|
|
||||||
|
$reader->req($message->id, $timeout);
|
||||||
|
},
|
||||||
|
static function () use ($reader, $message): void {
|
||||||
|
$reader->touch($message->id);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (true === yield $envelop) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$reader->rdy(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$reader->close();
|
||||||
|
}
|
||||||
|
}
|
68
src/Writer.php
Normal file
68
src/Writer.php
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Nsq;
|
||||||
|
|
||||||
|
use function array_map;
|
||||||
|
use function implode;
|
||||||
|
use function pack;
|
||||||
|
use function sprintf;
|
||||||
|
use const PHP_EOL;
|
||||||
|
|
||||||
|
final class Writer
|
||||||
|
{
|
||||||
|
private Connection $connection;
|
||||||
|
|
||||||
|
public function __construct(Connection $connection)
|
||||||
|
{
|
||||||
|
$this->connection = $connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @psalm-suppress PossiblyFalseOperand
|
||||||
|
*/
|
||||||
|
public function pub(string $topic, string $body): void
|
||||||
|
{
|
||||||
|
$size = pack('N', \strlen($body));
|
||||||
|
|
||||||
|
$buffer = 'PUB '.$topic.PHP_EOL.$size.$body;
|
||||||
|
|
||||||
|
$this->connection->write($buffer);
|
||||||
|
$this->connection->read();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @psalm-param array<mixed, mixed> $bodies
|
||||||
|
*
|
||||||
|
* @psalm-suppress PossiblyFalseOperand
|
||||||
|
*/
|
||||||
|
public function mpub(string $topic, array $bodies): void
|
||||||
|
{
|
||||||
|
$num = pack('N', \count($bodies));
|
||||||
|
|
||||||
|
$mb = implode('', array_map(static function ($body): string {
|
||||||
|
return pack('N', \strlen($body)).$body;
|
||||||
|
}, $bodies));
|
||||||
|
|
||||||
|
$size = pack('N', \strlen($num.$mb));
|
||||||
|
|
||||||
|
$buffer = 'MPUB '.$topic.PHP_EOL.$size.$num.$mb;
|
||||||
|
|
||||||
|
$this->connection->write($buffer);
|
||||||
|
$this->connection->read();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @psalm-suppress PossiblyFalseOperand
|
||||||
|
*/
|
||||||
|
public function dpub(string $topic, int $deferTime, string $body): void
|
||||||
|
{
|
||||||
|
$size = pack('N', \strlen($body));
|
||||||
|
|
||||||
|
$buffer = sprintf('DPUB %s %s', $topic, $deferTime).PHP_EOL.$size.$body;
|
||||||
|
|
||||||
|
$this->connection->write($buffer);
|
||||||
|
$this->connection->read();
|
||||||
|
}
|
||||||
|
}
|
35
tests/NsqTest.php
Normal file
35
tests/NsqTest.php
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Nsq\Config;
|
||||||
|
use Nsq\Connection;
|
||||||
|
use Nsq\Envelope;
|
||||||
|
use Nsq\Reader;
|
||||||
|
use Nsq\Subscriber;
|
||||||
|
use Nsq\Writer;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class NsqTest extends TestCase
|
||||||
|
{
|
||||||
|
public function test(): void
|
||||||
|
{
|
||||||
|
$config = new Config('tcp://localhost:4150');
|
||||||
|
|
||||||
|
$writer = new Writer(Connection::connect($config));
|
||||||
|
$writer->pub(__FUNCTION__, __FUNCTION__);
|
||||||
|
|
||||||
|
$subscriber = new Subscriber(new Reader(Connection::connect($config)));
|
||||||
|
$generator = $subscriber->subscribe(__FUNCTION__, __FUNCTION__, 1);
|
||||||
|
|
||||||
|
$envelope = $generator->current();
|
||||||
|
|
||||||
|
static::assertInstanceOf(Envelope::class, $envelope);
|
||||||
|
/** @var Envelope $envelope */
|
||||||
|
static::assertSame(__FUNCTION__, $envelope->message->body);
|
||||||
|
$envelope->ack();
|
||||||
|
|
||||||
|
$generator->next();
|
||||||
|
static::assertNull($generator->current());
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user